Skip to content

feat(db): add support for non-node libsql client #14204

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 37 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
a66455f
feat(db): add support for non-node libsql client
Adammatthiesen Aug 8, 2025
2ddea20
feat(db): update remote database info retrieval to include mode param…
Adammatthiesen Aug 8, 2025
4311aa6
feat(db): export AstroDBConfig type alongside integration from core i…
Adammatthiesen Aug 8, 2025
6ad1ac8
test(db): ensure mode parameter is passed to getRemoteDatabaseInfo in…
Adammatthiesen Aug 8, 2025
e34acea
feat(db): refactor database client handling and remove legacy code
Adammatthiesen Aug 9, 2025
12a4379
test(db): remove mode parameter from getRemoteDatabaseInfo assertions
Adammatthiesen Aug 9, 2025
d9bd4b1
test(db): remove mode parameter from getRemoteDatabaseInfo call in tests
Adammatthiesen Aug 9, 2025
322b7cf
fix(db): ensure correct handling of 'node' mode in client module func…
Adammatthiesen Aug 9, 2025
475c624
refactor(db): consolidate utility functions by moving hasPrimaryKey t…
Adammatthiesen Aug 9, 2025
604648e
fix(db): re-export hasPrimaryKey from utils for consistency
Adammatthiesen Aug 9, 2025
4187ecc
fix(db): update import path for hasPrimaryKey to utils for consistency
Adammatthiesen Aug 9, 2025
f3572c8
fix(db): update client import to use VIRTUAL_CLIENT_MODULE_ID for con…
Adammatthiesen Aug 9, 2025
be17d1e
fix(db): remove commented export of hasPrimaryKey for clarity
Adammatthiesen Aug 9, 2025
2a4f737
fix(db): update DB_CLIENTS paths to use '@astrojs' for consistency
Adammatthiesen Aug 9, 2025
4b661ab
fix(db): update DB_CLIENTS paths to use PACKAGE_NAME for consistency
Adammatthiesen Aug 9, 2025
d00fc28
fix(db): simplify db-client export paths for consistency
Adammatthiesen Aug 9, 2025
5b2c056
fix(db): unify client creation function names across db-client modules
Adammatthiesen Aug 9, 2025
3bc6eac
fix(db): standardize client creation function names across db-client …
Adammatthiesen Aug 9, 2025
24a49b2
fix(db): rename remote database client import for consistency
Adammatthiesen Aug 9, 2025
92516ee
fix(db): change const to let for parsedUrl in createClient function
Adammatthiesen Aug 9, 2025
c9e5592
fix(db): update variable naming for consistency in createClient function
Adammatthiesen Aug 9, 2025
736b2d2
fix(test): add logging for prodDbPath and ASTRO_DB_REMOTE_URL in libs…
Adammatthiesen Aug 9, 2025
03892a8
fix(test): add logging for absoluteFileUrl in libsql-remote tests
Adammatthiesen Aug 9, 2025
95bf697
fix(db): correct variable reference for rawUrl in createClient function
Adammatthiesen Aug 9, 2025
95dcef0
fix(test): remove debug logging for prodDbPath in libsql-remote tests
Adammatthiesen Aug 9, 2025
d7795eb
fix(db): rename __execute to localExecution for clarity in virtual mo…
Adammatthiesen Aug 9, 2025
8e4e5c5
refactor(db): replace parseOpts with parseLibSQLConfig for improved c…
Adammatthiesen Aug 9, 2025
400ea39
fix(db): prevent redundant assignment for 'url' in LibSQL config parsing
Adammatthiesen Aug 9, 2025
cd848b6
refactor(db): streamline libSQL configuration handling in createClien…
Adammatthiesen Aug 9, 2025
910081a
fix(test): remove redundant 'url' field from config assertions in db-…
Adammatthiesen Aug 9, 2025
89153ec
refactor(db): enhance boolean parsing logic in LibSQL config handling
Adammatthiesen Aug 9, 2025
272ed52
Merge branch 'main' into main
Adammatthiesen Aug 10, 2025
013d98a
Merge branch 'main' into main
Adammatthiesen Aug 10, 2025
f0311bc
Apply suggestions from code review
Adammatthiesen Aug 11, 2025
9fde9ec
Apply suggestions from code review
Adammatthiesen Aug 11, 2025
5604a6f
Refactor localExecution handling in vitePluginDb and related functions
Adammatthiesen Aug 11, 2025
c45022e
Update .changeset/full-dingos-repeat.md
Adammatthiesen Aug 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/full-dingos-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@astrojs/db': minor
---

Adds support for environments such as Cloudflare or Deno that require a non-node based libsql client.

To utilize this new feature, you must add the following to your Astro Db config. This will enable the usage of the alterative LibSQL web driver. In most cases this should only be needed on Cloudflare or Deno type environments, and using the default mode `node` will be enough for normal usage.

```ts
import db from '@astrojs/db';
import { defineConfig } from 'astro/config';

// https://astro.build/config
export default defineConfig({
integrations: [db({ mode: 'web' })],
});
```
4 changes: 3 additions & 1 deletion packages/db/src/core/cli/commands/execute/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,19 @@ export async function cmd({

let virtualModContents: string;
if (flags.remote) {
const dbInfo = getRemoteDatabaseInfo();
const dbInfo = getRemoteDatabaseInfo('node');
virtualModContents = getRemoteVirtualModContents({
tables: dbConfig.tables ?? {},
appToken: flags.token ?? dbInfo.token,
isBuild: false,
output: 'server',
mode: 'node'
});
} else {
virtualModContents = getLocalVirtualModContents({
tables: dbConfig.tables ?? {},
root: astroConfig.root,
mode: 'node'
});
}
const { code } = await bundleFile({ virtualModContents, root: astroConfig.root, fileUrl });
Expand Down
3 changes: 2 additions & 1 deletion packages/db/src/core/cli/commands/push/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export async function cmd({
}) {
const isDryRun = flags.dryRun;
const isForceReset = flags.forceReset;
const dbInfo = getRemoteDatabaseInfo();
const dbInfo = getRemoteDatabaseInfo('node');
const productionSnapshot = await getProductionCurrentSnapshot(dbInfo);
const currentSnapshot = createCurrentSnapshot(dbConfig);
const isFromScratch = !productionSnapshot;
Expand Down Expand Up @@ -111,6 +111,7 @@ async function pushToDb(requestBody: RequestBody, appToken: string, remoteUrl: s
const client = createRemoteDatabaseClient({
token: appToken,
url: remoteUrl,
mode: 'node',
});

await client.run(sql`create table if not exists _astro_db_snapshot (
Expand Down
2 changes: 1 addition & 1 deletion packages/db/src/core/cli/commands/shell/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export async function cmd({
console.error(SHELL_QUERY_MISSING_ERROR);
process.exit(1);
}
const dbInfo = getRemoteDatabaseInfo();
const dbInfo = getRemoteDatabaseInfo('node');
if (flags.remote) {
const db = createRemoteDatabaseClient(dbInfo);
const result = await db.run(sql.raw(query));
Expand Down
2 changes: 1 addition & 1 deletion packages/db/src/core/cli/commands/verify/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export async function cmd({
flags: Arguments;
}) {
const isJson = flags.json;
const dbInfo = getRemoteDatabaseInfo();
const dbInfo = getRemoteDatabaseInfo('node');
const productionSnapshot = await getProductionCurrentSnapshot(dbInfo);
const currentSnapshot = createCurrentSnapshot(dbConfig);
const { queries: migrationQueries, confirmations } = await getMigrationQueries({
Expand Down
1 change: 1 addition & 0 deletions packages/db/src/core/cli/migration-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@ async function getDbCurrentSnapshot(
const client = createRemoteDatabaseClient({
token: appToken,
url: remoteUrl,
mode: 'node',
});

try {
Expand Down
31 changes: 27 additions & 4 deletions packages/db/src/core/integration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
type ViteDevServer,
} from 'vite';
import parseArgs from 'yargs-parser';
import { z } from 'zod';
import { AstroDbError, isDbError } from '../../runtime/utils.js';
import { CONFIG_FILE_NAMES, DB_PATH, VIRTUAL_MODULE_ID } from '../consts.js';
import { EXEC_DEFAULT_EXPORT_ERROR, EXEC_ERROR } from '../errors.js';
Expand All @@ -28,7 +29,27 @@ import {
vitePluginDb,
} from './vite-plugin-db.js';

function astroDBIntegration(): AstroIntegration {
const astroDBConfigSchema = z
.object({
/**
* Sets the mode of the underlying `@libsql/client` connection.
*
* In most cases, the default 'node' mode is sufficient. On platforms like Cloudflare, or Deno, you may need to set this to 'web'.
*
* @default 'node'
*/
mode: z
.union([z.literal('node'), z.literal('web')])
.optional()
.default('node'),
})
.optional()
.default({});

export type AstroDBConfig = z.infer<typeof astroDBConfigSchema>;

function astroDBIntegration(options?: AstroDBConfig): AstroIntegration {
const resolvedConfig = astroDBConfigSchema.parse(options);
let connectToRemote = false;
let configFileDependencies: string[] = [];
let root: URL;
Expand Down Expand Up @@ -72,12 +93,13 @@ function astroDBIntegration(): AstroIntegration {
if (connectToRemote) {
dbPlugin = vitePluginDb({
connectToRemote,
appToken: getRemoteDatabaseInfo().token,
appToken: getRemoteDatabaseInfo(resolvedConfig.mode).token,
tables,
root: config.root,
srcDir: config.srcDir,
output: config.output,
seedHandler,
mode: resolvedConfig.mode,
});
} else {
dbPlugin = vitePluginDb({
Expand All @@ -89,6 +111,7 @@ function astroDBIntegration(): AstroIntegration {
output: config.output,
logger,
seedHandler,
mode: resolvedConfig.mode,
});
}

Expand Down Expand Up @@ -182,8 +205,8 @@ function databaseFileEnvDefined() {
return env.ASTRO_DATABASE_FILE != null || process.env.ASTRO_DATABASE_FILE != null;
}

export function integration(): AstroIntegration[] {
return [astroDBIntegration(), fileURLIntegration()];
export function integration(options?: AstroDBConfig): AstroIntegration[] {
return [astroDBIntegration(options), fileURLIntegration()];
}

async function executeSeedFile({
Expand Down
14 changes: 11 additions & 3 deletions packages/db/src/core/integration/vite-plugin-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type VitePluginDBParams =
logger?: AstroIntegrationLogger;
output: AstroConfig['output'];
seedHandler: SeedHandler;
mode: 'node' | 'web';
}
| {
connectToRemote: true;
Expand All @@ -51,6 +52,7 @@ type VitePluginDBParams =
root: URL;
output: AstroConfig['output'];
seedHandler: SeedHandler;
mode: 'node' | 'web';
};

export function vitePluginDb(params: VitePluginDBParams): VitePlugin {
Expand All @@ -77,6 +79,7 @@ export function vitePluginDb(params: VitePluginDBParams): VitePlugin {
tables: params.tables.get(),
isBuild: command === 'build',
output: params.output,
mode: params.mode,
});
}

Expand All @@ -87,6 +90,7 @@ export function vitePluginDb(params: VitePluginDBParams): VitePlugin {
return getLocalVirtualModContents({
root: params.root,
tables: params.tables.get(),
mode: 'node',
});
}

Expand All @@ -108,6 +112,7 @@ export function vitePluginDb(params: VitePluginDBParams): VitePlugin {
return getLocalVirtualModContents({
root: params.root,
tables: params.tables.get(),
mode: params.mode,
});
},
};
Expand All @@ -117,9 +122,9 @@ export function getConfigVirtualModContents() {
return `export * from ${RUNTIME_VIRTUAL_IMPORT}`;
}

export function getLocalVirtualModContents({ tables, root }: { tables: DBTables; root: URL }) {
export function getLocalVirtualModContents({ tables, root, mode }: { tables: DBTables; root: URL; mode: 'node' | 'web' }) {
const { ASTRO_DATABASE_FILE } = getAstroEnv();
const dbInfo = getRemoteDatabaseInfo();
const dbInfo = getRemoteDatabaseInfo(mode);
const dbUrl = new URL(DB_PATH, root);
return `
import { asDrizzleTable, createLocalDatabaseClient, normalizeDatabaseUrl } from ${RUNTIME_IMPORT};
Expand All @@ -137,13 +142,15 @@ export function getRemoteVirtualModContents({
appToken,
isBuild,
output,
mode
}: {
tables: DBTables;
appToken: string;
isBuild: boolean;
output: AstroConfig['output'];
mode: 'node' | 'web';
}) {
const dbInfo = getRemoteDatabaseInfo();
const dbInfo = getRemoteDatabaseInfo(mode);

function appTokenArg() {
if (isBuild) {
Expand Down Expand Up @@ -176,6 +183,7 @@ import {asDrizzleTable, createRemoteDatabaseClient} from ${RUNTIME_IMPORT};
export const db = await createRemoteDatabaseClient({
url: ${dbUrlArg()},
token: ${appTokenArg()},
mode: ${JSON.stringify(mode)},
});

export * from ${RUNTIME_VIRTUAL_IMPORT};
Expand Down
4 changes: 3 additions & 1 deletion packages/db/src/core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ export function getAstroEnv(envMode = ''): Record<`ASTRO_${string}`, string> {
export type RemoteDatabaseInfo = {
url: string;
token: string;
mode: 'node' | 'web';
};

export function getRemoteDatabaseInfo(): RemoteDatabaseInfo {
export function getRemoteDatabaseInfo(mode: 'node' | 'web'): RemoteDatabaseInfo {
const astroEnv = getAstroEnv();

return {
url: astroEnv.ASTRO_DB_REMOTE_URL,
token: astroEnv.ASTRO_DB_APP_TOKEN,
mode,
};
}

Expand Down
2 changes: 1 addition & 1 deletion packages/db/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { cli } from './core/cli/index.js';
export { integration as default } from './core/integration/index.js';
export { type AstroDBConfig, integration as default } from './core/integration/index.js';
export type { TableConfig } from './core/types.js';
31 changes: 28 additions & 3 deletions packages/db/src/runtime/db-client.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory @libsql/client automatically switches to the appropriate implementation for the runtime it is running on. So if the missing module is coming from LibSQL this is a bug there.

If it is coming from Drizzle Client's module then I don't know if it was supposed to work or not. Worth bringing it up on their discord or repo.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

My understanding would be that as well... but once i looked at how libsql client "decides" which one to use.... I think i found why it doesn't work... since when your deploying to CF, its still running node underlying when doing the import.

As for drizzle docs... they have this note

defaults to node import, automatically changes to web if target or platform is set for bundler, e.g. esbuild --platform=browser

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createClient, type Config as LibSQLConfig } from '@libsql/client';
import type { LibSQLDatabase } from 'drizzle-orm/libsql';
import { drizzle as drizzleLibsql } from 'drizzle-orm/libsql';
import { createClient as createClientWeb } from '@libsql/client/web';
import { drizzle as drizzleLibsql, type LibSQLDatabase } from 'drizzle-orm/libsql';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't Cloudflare complain just by importing this module? If that is the case, we'll need to do some tricks using virtual modules to only import the modules that are safe for the runtime.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah i was wondering the same thing... I'll convert the db client config parts into dedicated internal virtual modules that will be enabled depending on mode instead, here after i make my coffee.

import { drizzle as drizzleLibsqlWeb } from 'drizzle-orm/libsql/web';

const isWebContainer = !!process.versions?.webcontainer;

Expand All @@ -18,12 +19,18 @@ export function createLocalDatabaseClient(options: LocalDbClientOptions): LibSQL
type RemoteDbClientOptions = {
token: string;
url: string | URL;
mode: 'node' | 'web';
};

export function createRemoteDatabaseClient(options: RemoteDbClientOptions) {
const url = new URL(options.url);

return createRemoteLibSQLClient(options.token, url, options.url.toString());
switch (options.mode) {
case 'web':
return createRemoteLibSQLWebClient(options.token, url);
case 'node':
return createRemoteLibSQLClient(options.token, url, options.url.toString());
}
}

// this function parses the options from a `Record<string, string>`
Expand Down Expand Up @@ -69,3 +76,21 @@ function createRemoteLibSQLClient(authToken: string, dbURL: URL, rawUrl: string)
const client = createClient({ ...parseOpts(options), url, authToken });
return drizzleLibsql(client);
}

function createRemoteLibSQLWebClient(authToken: string, dbURL: URL) {
const options: Record<string, string> = Object.fromEntries(dbURL.searchParams.entries());
dbURL.search = '';

let url = dbURL.toString();

const supportedProtocols = ['http:', 'https:', 'libsql:'];

if (!supportedProtocols.includes(dbURL.protocol)) {
throw new Error(
`Unsupported protocol "${dbURL.protocol}" for libSQL web client. Supported protocols are: ${supportedProtocols.join(', ')}.`
);
}

const client = createClientWeb({ ...parseOpts(options), url, authToken });
return drizzleLibsqlWeb(client);
}
6 changes: 4 additions & 2 deletions packages/db/test/unit/remote-info.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,24 @@ describe('RemoteDatabaseInfo', () => {
});

test('default remote info', () => {
const dbInfo = getRemoteDatabaseInfo();
const dbInfo = getRemoteDatabaseInfo('node');

assert.deepEqual(dbInfo, {
url: undefined,
token: undefined,
mode: 'node',
});
});

test('configured libSQL remote', () => {
process.env.ASTRO_DB_REMOTE_URL = 'libsql://libsql.self.hosted';
process.env.ASTRO_DB_APP_TOKEN = 'foo';
const dbInfo = getRemoteDatabaseInfo();
const dbInfo = getRemoteDatabaseInfo('node');

assert.deepEqual(dbInfo, {
url: 'libsql://libsql.self.hosted',
token: 'foo',
mode: 'node'
});
});
});
Loading