feat: v2.5.0 T1 — libSQL (Turso) platform adapter + Docker Turso opt-in (#326)

* feat: add libSQL (Turso) platform adapter and Docker opt-in

- server/platform/libsql.ts: createLibsqlPlatform() using @libsql/client +
  drizzle-orm/libsql; accepts plain env record; async migrate at boot;
  authToken optional for file:// URLs
- server/entry-node.ts: select platform at startup — libsql when
  TURSO_DATABASE_URL is set, otherwise existing SQLite via createNodePlatform()
- drizzle.config.ts: switch to turso dialect when TURSO_DATABASE_URL is set
- vitest.libsql.config.ts + server/platform/libsql.libsql-test.ts: smoke suite
  covering connect, migrations, insert/select against users + storages tables
- package.json: add @libsql/client dependency; add test:libsql script;
  externalize @libsql/client in build:node tsup command
- vitest.config.ts: exclude *.libsql-test.ts from coverage
- docs/deploy/docker.md: document Turso opt-in with copy-pasteable
  docker-compose snippet
- CONTRIBUTING.md: add Turso migrate path paragraph under Database Migrations

Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f

* refactor: turn bootstrap.ts into a Platform-accepting factory

- server/bootstrap.ts: replace singleton module-scope script with
  exportable createBootstrap(platform) async factory; reads
  BETTER_AUTH_SECRET/BETTER_AUTH_URL/TRUSTED_ORIGINS from platform.getEnv
  so every future entry (Lambda, Vercel, Netlify, Azure) can reuse it
- server/entry-node.ts: slim down to platform selection + createBootstrap
  call; no more duplicate auth/app wiring
- server/dev.ts: thin vite-dev-server entry that creates NodePlatform and
  calls createBootstrap; replaces the former default export in bootstrap.ts
- vite.config.ts: update node dev server entry to server/dev.ts
- server/platform/libsql.ts: fix getEnv to check env record before
  falling back to process.env, matching the cloudflare.ts pattern

Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f

* style: apply biome auto-fixes for pre-existing lint issues

Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f

---------

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
This commit is contained in:
Jasper Van
2026-04-22 00:48:21 -04:00
committed by GitHub
parent 5bd29b5e8c
commit 8005defd97
13 changed files with 619 additions and 27 deletions
+18
View File
@@ -93,6 +93,24 @@ npm run db:reset:d1 # Reset D1 local database (.wrangler)
Migration files live in `migrations/` at project root. Always commit them.
### Turso (libSQL) migrate path
When deploying the Node/Docker image against a Turso (libSQL) database, set `TURSO_DATABASE_URL` (and `TURSO_AUTH_TOKEN` for remote URLs) before running `db:migrate`. `drizzle.config.ts` detects the env var and switches to the `turso` dialect automatically:
```sh
TURSO_DATABASE_URL=libsql://your-db.turso.io \
TURSO_AUTH_TOKEN=your-token \
npm run db:migrate
```
For local libSQL files the token can be omitted:
```sh
TURSO_DATABASE_URL=file:./zpan.db npm run db:migrate
```
Migrations run automatically at Docker container startup when `TURSO_DATABASE_URL` is set. See [docs/deploy/docker.md](docs/deploy/docker.md) for the full Docker + Turso setup.
## Deployment
Primary target is Cloudflare Workers. Node.js (Docker) is the backup runtime.
+70
View File
@@ -0,0 +1,70 @@
# Docker Deployment
ZPan ships as a single Docker image. By default it uses an embedded SQLite database (`better-sqlite3`). For production multi-replica deployments you can opt into [Turso](https://turso.tech) (libSQL) as a shared remote database.
## Default: local SQLite
No extra configuration needed. Mount a volume so the database survives container restarts:
```yaml
services:
zpan:
image: ghcr.io/saltbo/zpan:latest
ports:
- "8222:8222"
environment:
PORT: 8222
BETTER_AUTH_SECRET: <generate with: openssl rand -base64 32>
BETTER_AUTH_URL: https://your-domain.example
DATABASE_URL: /data/zpan.db
volumes:
- zpan-data:/data
restart: unless-stopped
volumes:
zpan-data:
```
Migrations run automatically at startup.
## Turso (libSQL) opt-in
Set `TURSO_DATABASE_URL` to switch from local SQLite to a Turso (or self-hosted libSQL) database. `TURSO_AUTH_TOKEN` is required for remote URLs; it can be omitted for local `file://` URLs.
```yaml
services:
zpan:
image: ghcr.io/saltbo/zpan:latest
ports:
- "8222:8222"
environment:
PORT: 8222
BETTER_AUTH_SECRET: <generate with: openssl rand -base64 32>
BETTER_AUTH_URL: https://your-domain.example
TURSO_DATABASE_URL: libsql://your-db-name-orgname.turso.io
TURSO_AUTH_TOKEN: <your-turso-auth-token>
restart: unless-stopped
```
When `TURSO_DATABASE_URL` is present:
- `DATABASE_URL` is ignored.
- Migrations are applied automatically at startup via `drizzle-orm/libsql/migrator`.
- `TURSO_AUTH_TOKEN` may be omitted only for `file://` URLs (local libSQL files).
### Running migrations manually against Turso
```sh
TURSO_DATABASE_URL=libsql://your-db.turso.io \
TURSO_AUTH_TOKEN=your-token \
npm run db:migrate
```
`drizzle.config.ts` automatically switches to the `turso` dialect when `TURSO_DATABASE_URL` is set, so `npm run db:generate` and `npm run db:migrate` work against Turso without any extra flags.
### Obtaining a Turso auth token
```sh
turso db tokens create your-db-name
```
Or create one in the [Turso dashboard](https://app.turso.tech).
+20 -8
View File
@@ -1,10 +1,22 @@
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
schema: ['./server/db/schema.ts', './server/db/auth-schema.ts'],
out: './migrations',
dialect: 'sqlite',
dbCredentials: {
url: process.env.DATABASE_URL || 'zpan.db',
},
})
const tursoUrl = process.env.TURSO_DATABASE_URL
export default tursoUrl
? defineConfig({
schema: ['./server/db/schema.ts', './server/db/auth-schema.ts'],
out: './migrations',
dialect: 'turso',
dbCredentials: {
url: tursoUrl,
authToken: process.env.TURSO_AUTH_TOKEN,
},
})
: defineConfig({
schema: ['./server/db/schema.ts', './server/db/auth-schema.ts'],
out: './migrations',
dialect: 'sqlite',
dbCredentials: {
url: process.env.DATABASE_URL || 'zpan.db',
},
})
+372 -3
View File
@@ -17,6 +17,7 @@
"@hono/node-server": "^1.13.0",
"@hono/zod-validator": "^0.7.6",
"@hookform/resolvers": "^5.2.2",
"@libsql/client": "^0.17.2",
"@opentelemetry/api": "^1.9.1",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.0",
@@ -3582,6 +3583,167 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@libsql/client": {
"version": "0.17.2",
"resolved": "https://registry.npmjs.org/@libsql/client/-/client-0.17.2.tgz",
"integrity": "sha512-0aw0S3iQMHvOxfRt5j1atoCCPMT3gjsB2PS8/uxSM1DcDn39xqz6RlgSMxtP8I3JsxIXAFuw7S41baLEw0Zi+Q==",
"license": "MIT",
"dependencies": {
"@libsql/core": "^0.17.2",
"@libsql/hrana-client": "^0.9.0",
"js-base64": "^3.7.5",
"libsql": "^0.5.28",
"promise-limit": "^2.7.0"
}
},
"node_modules/@libsql/core": {
"version": "0.17.2",
"resolved": "https://registry.npmjs.org/@libsql/core/-/core-0.17.2.tgz",
"integrity": "sha512-L8qv12HZ/jRBcETVR3rscP0uHNxh+K3EABSde6scCw7zfOdiLqO3MAkJaeE1WovPsjXzsN/JBoZED4+7EZVT3g==",
"license": "MIT",
"dependencies": {
"js-base64": "^3.7.5"
}
},
"node_modules/@libsql/darwin-arm64": {
"version": "0.5.29",
"resolved": "https://registry.npmjs.org/@libsql/darwin-arm64/-/darwin-arm64-0.5.29.tgz",
"integrity": "sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@libsql/darwin-x64": {
"version": "0.5.29",
"resolved": "https://registry.npmjs.org/@libsql/darwin-x64/-/darwin-x64-0.5.29.tgz",
"integrity": "sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@libsql/hrana-client": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@libsql/hrana-client/-/hrana-client-0.9.0.tgz",
"integrity": "sha512-pxQ1986AuWfPX4oXzBvLwBnfgKDE5OMhAdR/5cZmRaB4Ygz5MecQybvwZupnRz341r2CtFmbk/BhSu7k2Lm+Jw==",
"license": "MIT",
"dependencies": {
"@libsql/isomorphic-ws": "^0.1.5",
"cross-fetch": "^4.0.0",
"js-base64": "^3.7.5",
"node-fetch": "^3.3.2"
}
},
"node_modules/@libsql/isomorphic-ws": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/@libsql/isomorphic-ws/-/isomorphic-ws-0.1.5.tgz",
"integrity": "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==",
"license": "MIT",
"dependencies": {
"@types/ws": "^8.5.4",
"ws": "^8.13.0"
}
},
"node_modules/@libsql/linux-arm-gnueabihf": {
"version": "0.5.29",
"resolved": "https://registry.npmjs.org/@libsql/linux-arm-gnueabihf/-/linux-arm-gnueabihf-0.5.29.tgz",
"integrity": "sha512-CD4n4zj7SJTHso4nf5cuMoWoMSS7asn5hHygsDuhRl8jjjCTT3yE+xdUvI4J7zsyb53VO5ISh4cwwOtf6k2UhQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@libsql/linux-arm-musleabihf": {
"version": "0.5.29",
"resolved": "https://registry.npmjs.org/@libsql/linux-arm-musleabihf/-/linux-arm-musleabihf-0.5.29.tgz",
"integrity": "sha512-2Z9qBVpEJV7OeflzIR3+l5yAd4uTOLxklScYTwpZnkm2vDSGlC1PRlueLaufc4EFITkLKXK2MWBpexuNJfMVcg==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@libsql/linux-arm64-gnu": {
"version": "0.5.29",
"resolved": "https://registry.npmjs.org/@libsql/linux-arm64-gnu/-/linux-arm64-gnu-0.5.29.tgz",
"integrity": "sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@libsql/linux-arm64-musl": {
"version": "0.5.29",
"resolved": "https://registry.npmjs.org/@libsql/linux-arm64-musl/-/linux-arm64-musl-0.5.29.tgz",
"integrity": "sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@libsql/linux-x64-gnu": {
"version": "0.5.29",
"resolved": "https://registry.npmjs.org/@libsql/linux-x64-gnu/-/linux-x64-gnu-0.5.29.tgz",
"integrity": "sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@libsql/linux-x64-musl": {
"version": "0.5.29",
"resolved": "https://registry.npmjs.org/@libsql/linux-x64-musl/-/linux-x64-musl-0.5.29.tgz",
"integrity": "sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@libsql/win32-x64-msvc": {
"version": "0.5.29",
"resolved": "https://registry.npmjs.org/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.5.29.tgz",
"integrity": "sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@napi-rs/canvas": {
"version": "0.1.97",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.97.tgz",
@@ -3832,6 +3994,12 @@
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@neon-rs/load": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@neon-rs/load/-/load-0.0.4.tgz",
"integrity": "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==",
"license": "MIT"
},
"node_modules/@noble/ciphers": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz",
@@ -9609,7 +9777,6 @@
"version": "25.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.19.0"
@@ -9651,6 +9818,15 @@
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@ungap/structured-clone": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
@@ -10616,6 +10792,57 @@
"integrity": "sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA==",
"license": "MIT"
},
"node_modules/cross-fetch": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz",
"integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==",
"license": "MIT",
"dependencies": {
"node-fetch": "^2.7.0"
}
},
"node_modules/cross-fetch/node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/cross-fetch/node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/cross-fetch/node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/cross-fetch/node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/css-tree": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
@@ -10637,6 +10864,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/data-urls": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
@@ -11653,6 +11889,29 @@
}
}
},
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/file-selector": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
@@ -11696,6 +11955,18 @@
"rollup": "^4.34.8"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"license": "MIT",
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@@ -12228,6 +12499,12 @@
"node": ">=10"
}
},
"node_modules/js-base64": {
"version": "3.7.8",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz",
"integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==",
"license": "BSD-3-Clause"
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -12340,6 +12617,47 @@
"node": ">=20.0.0"
}
},
"node_modules/libsql": {
"version": "0.5.29",
"resolved": "https://registry.npmjs.org/libsql/-/libsql-0.5.29.tgz",
"integrity": "sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg==",
"cpu": [
"x64",
"arm64",
"wasm32",
"arm"
],
"license": "MIT",
"os": [
"darwin",
"linux",
"win32"
],
"dependencies": {
"@neon-rs/load": "^0.0.4",
"detect-libc": "2.0.2"
},
"optionalDependencies": {
"@libsql/darwin-arm64": "0.5.29",
"@libsql/darwin-x64": "0.5.29",
"@libsql/linux-arm-gnueabihf": "0.5.29",
"@libsql/linux-arm-musleabihf": "0.5.29",
"@libsql/linux-arm64-gnu": "0.5.29",
"@libsql/linux-arm64-musl": "0.5.29",
"@libsql/linux-x64-gnu": "0.5.29",
"@libsql/linux-x64-musl": "0.5.29",
"@libsql/win32-x64-msvc": "0.5.29"
}
},
"node_modules/libsql/node_modules/detect-libc": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
"integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
@@ -13913,6 +14231,44 @@
"node": ">=10"
}
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"deprecated": "Use your platform's native DOMException instead",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"license": "MIT",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/node-readable-to-web-readable-stream": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/node-readable-to-web-readable-stream/-/node-readable-to-web-readable-stream-0.4.2.tgz",
@@ -14343,6 +14699,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/promise-limit": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz",
"integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==",
"license": "ISC"
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -15960,7 +16322,6 @@
"version": "7.19.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
"dev": true,
"license": "MIT"
},
"node_modules/unenv": {
@@ -16428,6 +16789,15 @@
"loose-envify": "^1.0.0"
}
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/webidl-conversions": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
@@ -16609,7 +16979,6 @@
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
+4 -2
View File
@@ -7,7 +7,7 @@
"dev": "CLOUDFLARE_ENV=staging vite dev",
"dev:node": "node --env-file=.dev.vars node_modules/vite/bin/vite.js dev --mode node",
"build": "[ \"$WORKERS_CI\" = \"1\" ] && [ \"$WORKERS_CI_BRANCH\" != \"master\" ] && export CLOUDFLARE_ENV=staging; vite build",
"build:node": "vite build --mode node && tsup server/entry-node.ts --format esm --outDir dist-server --external better-sqlite3",
"build:node": "vite build --mode node && tsup server/entry-node.ts --format esm --outDir dist-server --external better-sqlite3 --external @libsql/client",
"deploy": "npm run db:migrate:d1:prod && wrangler deploy",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
@@ -19,6 +19,7 @@
"typecheck": "tsc --noEmit -p tsconfig.server.json && tsc --noEmit -p tsconfig.web.json",
"test": "vitest run",
"test:cf": "vitest run --config vitest.cloudflare.config.ts",
"test:libsql": "vitest run --config vitest.libsql.config.ts",
"test:watch": "vitest",
"lint": "biome check .",
"lint:fix": "biome check --write .",
@@ -38,8 +39,8 @@
"@dnd-kit/utilities": "^3.2.2",
"@hono/node-server": "^1.13.0",
"@hono/zod-validator": "^0.7.6",
"better-sqlite3": "^11.0.0",
"@hookform/resolvers": "^5.2.2",
"@libsql/client": "^0.17.2",
"@opentelemetry/api": "^1.9.1",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.0",
@@ -53,6 +54,7 @@
"@tanstack/react-table": "^8.21.3",
"@vidstack/react": "^1.12.13",
"better-auth": "^1.6.2",
"better-sqlite3": "^11.0.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"drizzle-orm": "^0.45.2",
+16 -12
View File
@@ -1,16 +1,20 @@
import { createApp } from './app'
import { createAuth } from './auth'
import { createNodePlatform } from './platform/node'
import type { Platform } from './platform/interface'
const platform = createNodePlatform()
const secret = process.env.BETTER_AUTH_SECRET
if (!secret) {
throw new Error('BETTER_AUTH_SECRET is required. Set it in the environment before starting the server.')
export async function createBootstrap(platform: Platform) {
const secret = platform.getEnv('BETTER_AUTH_SECRET')
if (!secret) {
throw new Error('BETTER_AUTH_SECRET is required. Set it in the environment before starting the server.')
}
const baseURL = platform.getEnv('BETTER_AUTH_URL') || 'http://localhost:5173'
const trustedOrigins = platform
.getEnv('TRUSTED_ORIGINS')
?.split(',')
.map((o) => o.trim())
.filter(Boolean) || ['http://localhost:5173']
const auth = await createAuth(platform.db, secret, baseURL, trustedOrigins)
return createApp(platform, auth)
}
const baseURL = process.env.BETTER_AUTH_URL || 'http://localhost:5173'
const trustedOrigins = process.env.TRUSTED_ORIGINS?.split(',')
.map((o) => o.trim())
.filter(Boolean) || ['http://localhost:5173']
const auth = await createAuth(platform.db, secret, baseURL, trustedOrigins)
export default createApp(platform, auth)
+5
View File
@@ -0,0 +1,5 @@
import { createBootstrap } from './bootstrap'
import { createNodePlatform } from './platform/node'
const platform = createNodePlatform()
export default await createBootstrap(platform)
+12 -1
View File
@@ -1,7 +1,18 @@
import { serve } from '@hono/node-server'
import { serveStatic } from '@hono/node-server/serve-static'
import { Hono } from 'hono'
import app from './bootstrap'
import { createBootstrap } from './bootstrap'
import { createLibsqlPlatform } from './platform/libsql'
import { createNodePlatform } from './platform/node'
const platform = process.env.TURSO_DATABASE_URL
? await createLibsqlPlatform({
TURSO_DATABASE_URL: process.env.TURSO_DATABASE_URL,
TURSO_AUTH_TOKEN: process.env.TURSO_AUTH_TOKEN,
})
: createNodePlatform()
const app = await createBootstrap(platform)
const server = new Hono()
server.route('/', app)
+51
View File
@@ -0,0 +1,51 @@
import path from 'node:path'
import { beforeAll, describe, expect, it } from 'vitest'
import type { Platform } from './interface'
import { createLibsqlPlatform } from './libsql'
const migrationsFolder = path.join(__dirname, '../../migrations')
let platform: Platform
beforeAll(async () => {
process.env.MIGRATIONS_DIR = migrationsFolder
platform = await createLibsqlPlatform({
TURSO_DATABASE_URL: 'file::memory:',
})
}, 30_000)
describe('libsql platform', () => {
it('connects and applies all migrations', () => {
expect(platform).toBeDefined()
expect(platform.db).toBeDefined()
})
it('returns env values via getEnv', () => {
process.env.TEST_LIBSQL_KEY = 'hello'
expect(platform.getEnv('TEST_LIBSQL_KEY')).toBe('hello')
delete process.env.TEST_LIBSQL_KEY
})
it('returns undefined for missing env keys', () => {
expect(platform.getEnv('__NONEXISTENT_KEY__')).toBeUndefined()
})
it('can query the storages table', async () => {
const rows = await platform.db.query.storages.findMany()
expect(Array.isArray(rows)).toBe(true)
})
it('can query the user table', async () => {
const rows = await platform.db.query.user.findMany()
expect(Array.isArray(rows)).toBe(true)
})
})
describe('createLibsqlPlatform with file:// URL', () => {
it('boots without TURSO_AUTH_TOKEN', async () => {
const p = await createLibsqlPlatform({
TURSO_DATABASE_URL: 'file::memory:',
})
expect(p.db).toBeDefined()
})
})
+32
View File
@@ -0,0 +1,32 @@
import { createClient } from '@libsql/client'
import { drizzle } from 'drizzle-orm/libsql'
import { migrate } from 'drizzle-orm/libsql/migrator'
import * as authSchema from '../db/auth-schema'
import * as schema from '../db/schema'
import type { Platform } from './interface'
interface LibsqlEnv {
TURSO_DATABASE_URL: string
TURSO_AUTH_TOKEN?: string
}
export async function createLibsqlPlatform(env: LibsqlEnv): Promise<Platform> {
const migrationsFolder = process.env.MIGRATIONS_DIR || './migrations'
const envRecord: Record<string, string | undefined> = { ...env }
const client = createClient({
url: env.TURSO_DATABASE_URL,
authToken: env.TURSO_AUTH_TOKEN,
})
const db = drizzle(client, { schema: { ...schema, ...authSchema } })
await migrate(db, { migrationsFolder })
return {
db,
getEnv(key: string) {
return envRecord[key] ?? process.env[key]
},
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ export default defineConfig(({ mode }) => ({
react(),
tailwindcss(),
...(mode === 'node'
? [devServer({ entry: './server/bootstrap.ts', injectClientScript: false, exclude: [/^(?!\/api\/).*/] })]
? [devServer({ entry: './server/dev.ts', injectClientScript: false, exclude: [/^(?!\/api\/).*/] })]
: [cloudflare()]),
],
resolve: {
+1
View File
@@ -23,6 +23,7 @@ const coverageConfig = {
'server/**/*.test.ts',
'server/**/*.integration.test.ts',
'server/**/*.cf-test.ts',
'server/**/*.libsql-test.ts',
'server/test/**',
'server/platform/**',
'server/db/**',
+17
View File
@@ -0,0 +1,17 @@
import path from 'node:path'
import { defineConfig } from 'vitest/config'
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@shared': path.resolve(__dirname, './shared'),
'@server': path.resolve(__dirname, './server'),
},
},
test: {
globals: true,
testTimeout: 30_000,
include: ['server/**/*.libsql-test.ts'],
},
})