diff --git a/.env.example b/.env.example index d44e083..b5e1bdc 100644 --- a/.env.example +++ b/.env.example @@ -11,10 +11,6 @@ AI_API_KEY= # Optional: local AI endpoint for local AI mode LOCAL_AI_URL=http://127.0.0.1:3456 -# --- Turso (libSQL) --- -TURSO_DATABASE_URL= -TURSO_AUTH_TOKEN= - # --- better-auth --- # Generate with: openssl rand -base64 32 BETTER_AUTH_SECRET= diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1f7598d..410aaa3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -18,7 +18,7 @@ jobs: env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - REQUIRED_SECRETS: BETTER_AUTH_SECRET GOOGLE_CLIENT_ID GOOGLE_CLIENT_SECRET TURSO_AUTH_TOKEN TURSO_DATABASE_URL + REQUIRED_SECRETS: BETTER_AUTH_SECRET GOOGLE_CLIENT_ID GOOGLE_CLIENT_SECRET run: | set -euo pipefail secret_names="$(pnpm exec wrangler secret list --format json | node -e "let input='';process.stdin.on('data',c=>input+=c);process.stdin.on('end',()=>{const parsed=JSON.parse(input);console.log(parsed.map((item)=>item.name).join('\n'))})")" diff --git a/AGENTS.md b/AGENTS.md index 2004b72..a40e066 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,7 @@ auto-summarise the saved material. Companion Chrome MV3 extension. See ## Stack (one-liner) Vite + React 19 SPA (single `app.html` entry) + Hono Worker on Cloudflare -Workers (`src/worker.ts`), Turso (libSQL) via Drizzle ORM, better-auth Google +Workers (`src/worker.ts`), Cloudflare D1 via Drizzle ORM, better-auth Google OAuth, Cloudflare R2 for PDFs, free-ai-gateway + BYOK + local-ai dev bridge. No SSR, no Next.js, no Firebase. @@ -34,8 +34,8 @@ pnpm test # vitest run pnpm test:e2e # playwright pnpm lint # biome check . pnpm format # biome format --write . -pnpm db:push # drizzle-kit push (schema sync) -pnpm db:studio # drizzle-kit studio +pnpm db:generate # generate a tracked D1 migration +pnpm db:migrate:local # apply migrations to isolated local D1 pnpm docs:check # validate docs/ links + structure pnpm docs:dev # blume dev (local docs site; requires `pnpm add -D blume`) pnpm docs:build # blume build (presentation only; not part of production build) @@ -62,7 +62,7 @@ Full command map: [docs/development/commands.md](docs/development/commands.md). and `/api/*` to reach the Worker before the `ASSETS` binding. - **BYOK provider keys live in the browser only** — never persist or log server-side. `rdr_*` API keys are hashed at rest; plaintext shown once. -- **Schema changes are additive + deliberate.** `pnpm db:push` is schema-sync; +- **Schema changes are additive + deliberate.** Generate and inspect SQL before applying it; read the SQL under `drizzle/` before applying to production. See [docs/operations/runbooks/migrate-schema.md](docs/operations/runbooks/migrate-schema.md). - **Pre-commit hook (Husky + lint-staged)** runs `biome check --write` on diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md index 0f57f72..299a384 100644 --- a/PROJECT_STATUS.md +++ b/PROJECT_STATUS.md @@ -13,11 +13,17 @@ maintenance-first support. ## Dependencies -- Vite + React 19 SPA, Hono Worker, Turso + Drizzle, Cloudflare R2, better-auth - Google OAuth, and optional AI providers. +- Vite + React 19 SPA, Hono Worker, Cloudflare D1 + Drizzle, Cloudflare R2, + better-auth Google OAuth, and optional AI providers. Turso remains + temporarily preserved as the rollback source for the D1 cutover observation + window. ## Timeline +- **2026-08-01:** Migrated production relational persistence from Turso to a + project-owned Cloudflare D1 database after schema, row-count, aggregate, + ownership, auth/session, and document-path parity checks. R2 remains the PDF + object store; Turso is preserved for the bounded rollback observation window. - **2026-07-31:** Kept the public sitemap HTML-only and added source-level sitemap/catalog/Markdown parity coverage. - **2026-07-31:** Added locally verified Open Graph/Twitter image metadata and diff --git a/README.md b/README.md index 3f25b7d..91e952b 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ A modern web application for capturing and annotating articles with a distractio | Concern | Service | | ------------ | -------------------------------------------------------------------------------------------- | | Hosting | Cloudflare Workers (`reader`) via Hono Worker (`src/worker.ts`) | -| Database | Turso (libSQL) via Drizzle ORM | +| Database | Cloudflare D1 via Drizzle ORM | | Auth | better-auth + Google OAuth | | File storage | Cloudflare R2 (`reader-pdfs`, bound as `PDFS_BUCKET`) | | AI | free-ai-gateway (Workers AI chokepoint); BYOK providers (OpenAI/Anthropic/Gemini) + local AI | @@ -74,9 +74,9 @@ graph TB end subgraph "Backend Services" - Turso[Turso libSQL] - Turso --> Drizzle[Drizzle ORM] - Turso --> BetterAuth[better-auth - Google OAuth] + D1[Cloudflare D1] + D1 --> Drizzle[Drizzle ORM] + D1 --> BetterAuth[better-auth - Google OAuth] R2[Cloudflare R2 - PDFS_BUCKET] @@ -91,7 +91,7 @@ graph TB end UI --> API - API --> Turso + API --> D1 API --> R2 API --> External @@ -100,14 +100,14 @@ graph TB classDef external fill:#f59e0b,stroke:#d97706,color:#fff class UI,Styling,State frontend - class API,Articles,Projects,Auth,AI,Search,Tags,PDF,Snapshot,Turso,Drizzle,BetterAuth,R2 backend + class API,Articles,Projects,Auth,AI,Search,Tags,PDF,Snapshot,D1,Drizzle,BetterAuth,R2 backend class External,Readability,AIProviders,OpenAI,Anthropic,Gemini,Gateway,LocalAI external ``` ### Tech Stack - **Frontend**: Vite + React 19 SPA (single `app.html` entry, client-side routing via `react-router-dom`), TypeScript, Tailwind CSS v4 -- **Database**: Turso (libSQL) via Drizzle ORM +- **Database**: Cloudflare D1 via Drizzle ORM - **Auth**: better-auth (Google OAuth, Drizzle adapter) - **Storage**: Cloudflare R2 (PDFs) via Workers binding - **AI Integration**: Vercel AI SDK + AI Gateway (preferred), BYOK chat providers, local AI support @@ -119,7 +119,7 @@ graph TB ### Prerequisites - Node.js 22+ -- A Turso database (`turso db create`) and auth token +- Wrangler-managed local D1 (configured in `wrangler.local.toml`) - A Cloudflare account with an R2 bucket bound as `PDFS_BUCKET` - A Google OAuth client (Cloud Console > APIs & Services > Credentials) @@ -142,10 +142,6 @@ graph TB Edit `.env.local`: ```env - # Turso - TURSO_DATABASE_URL=libsql://your-db.turso.io - TURSO_AUTH_TOKEN=... - # better-auth BETTER_AUTH_SECRET=$(openssl rand -base64 32) BETTER_AUTH_URL=http://localhost:8787 @@ -162,10 +158,10 @@ graph TB LOCAL_AI_URL=http://127.0.0.1:3456 ``` -3. Push the schema to Turso: +3. Apply the schema to local D1: ```bash - pnpm db:push + pnpm db:migrate:local ``` 4. Run the development server: @@ -197,8 +193,8 @@ pnpm deploy # validate env + cf:build + wrangler deploy pnpm lint # Run ESLint pnpm format # Format code with Biome pnpm typecheck # tsc --noEmit (app + worker tsconfigs) -pnpm db:push # Apply schema to Turso (Drizzle) -pnpm db:studio # Open Drizzle Studio +pnpm db:generate # Generate a tracked D1 migration +pnpm db:migrate:local # Apply migrations to local D1 only ``` ## Deployment @@ -211,8 +207,8 @@ pnpm deploy This runs `cf:build` (Vite SPA build + Astro landing overlay into `dist/`) and `wrangler deploy`. Configure secrets via `wrangler secret put` for `BETTER_AUTH_SECRET`, -`TURSO_AUTH_TOKEN`, `GOOGLE_CLIENT_SECRET`, etc., and bind the R2 bucket as -`PDFS_BUCKET` in `wrangler.toml`. +`GOOGLE_CLIENT_SECRET`, etc., and bind D1 as `DB` plus R2 as `PDFS_BUCKET` in +`wrangler.toml`. ## Project Structure @@ -231,7 +227,7 @@ web-annotator/ │ ├── lib/ │ │ ├── auth.ts # better-auth server config │ │ ├── auth-client.ts# better-auth browser client -│ │ ├── db/ # Drizzle schema + Turso client +│ │ ├── db/ # Drizzle schema + D1 client │ │ └── storage.ts # R2 helpers │ └── types.ts # TypeScript definitions ├── packages/ diff --git a/docs/architecture/data-flow.md b/docs/architecture/data-flow.md index 7348619..b06fd1e 100644 --- a/docs/architecture/data-flow.md +++ b/docs/architecture/data-flow.md @@ -38,9 +38,8 @@ accept API keys (e.g. key management itself). ## Database (`src/lib/db/`) -- `createDb(env)` builds a Drizzle instance over `@libsql/client/web` using - `TURSO_DATABASE_URL` (rewritten `libsql://` → `https://`) and - `TURSO_AUTH_TOKEN`. +- `createDb(env)` builds a Drizzle instance directly over the request's + Cloudflare D1 `DB` binding. - A module-level `db` Proxy defers client creation until first property access (`getDb()`), so the Worker can `bindWorkerEnv()` before any query runs. - All reads/writes are scoped by `userId` from the auth step. JSON columns @@ -61,8 +60,8 @@ accept API keys (e.g. key management itself). - `api_keys` — `rdr_*` tokens; `token_hash` unique; `revoked_at` nullable. Migrations live in `drizzle/` (`0000_baseline.sql`, `0001_memories.sql`, -`0002_first_green_goblin.sql` for RSS). Schema sync uses `drizzle-kit push` -(`pnpm db:push`); see [operations/runbooks/migrate-schema.md](../operations/runbooks/migrate-schema.md). +`0002_first_green_goblin.sql` for RSS). Apply them locally with +`pnpm db:migrate:local`; see [operations/runbooks/migrate-schema.md](../operations/runbooks/migrate-schema.md). ## PDF storage (`src/lib/storage.ts` + `src/worker/routes/pdf.ts`) diff --git a/docs/architecture/decisions/0002-turso-drizzle.md b/docs/architecture/decisions/0002-turso-drizzle.md index 78766a3..1d03908 100644 --- a/docs/architecture/decisions/0002-turso-drizzle.md +++ b/docs/architecture/decisions/0002-turso-drizzle.md @@ -1,7 +1,7 @@ # ADR-0002: Turso (libSQL) via Drizzle ORM **Date:** 2026-04-25 (Firebase → Turso migration); carried forward post-Vite migration -**Status:** Current +**Status:** Superseded by [ADR-0010](0010-cloudflare-d1.md); retained as migration history. **Supersedes:** [archive/decisions.md ADR-02](../../archive/decisions.md) (same decision; this record updates the runtime context to Vite + Hono) ## Context diff --git a/docs/architecture/decisions/0010-cloudflare-d1.md b/docs/architecture/decisions/0010-cloudflare-d1.md new file mode 100644 index 0000000..699a5ec --- /dev/null +++ b/docs/architecture/decisions/0010-cloudflare-d1.md @@ -0,0 +1,28 @@ +# ADR-0010: Cloudflare D1 for structured data + +**Status:** Accepted for migration; production cutover remains operator-gated. + +## Decision + +Use one Cloudflare D1 database, bound as `DB`, for Reader's application and +better-auth tables. Keep Drizzle as the query and schema layer. Generate tracked +SQLite migrations and apply them through Wrangler's D1 migration ledger. + +Local development uses the isolated binding in `wrangler.local.toml`. Remote +resource creation, data import, production binding, deployment, and Turso +retirement are separate approved operations. + +## Why + +Reader already runs as a Cloudflare Worker and stores PDFs in R2. D1 removes +the external libSQL client and database credentials from each request while +keeping the existing SQLite schema and Drizzle query surface. + +## Consequences + +- `src/lib/db/client.ts` receives a request-scoped D1 binding. +- Better Auth and all application tables remain in the same database. +- The legacy Firestore import is local by default and requires both `--apply` + and `--remote` before it can write remotely. +- Production Turso remains the authority until parity checks and explicit + cutover approval pass; its retirement is not implied by cutover. diff --git a/docs/architecture/how-it-works.md b/docs/architecture/how-it-works.md index cb25e79..268777a 100644 --- a/docs/architecture/how-it-works.md +++ b/docs/architecture/how-it-works.md @@ -22,7 +22,7 @@ each choice. A **Vite + React 19 single-page app** (no SSR) runs entirely in the browser and talks to a **single Hono Worker** on Cloudflare, which routes `/api/*` to resource handlers, serves the built SPA over the `ASSETS` binding, and fans out -to three backends: **Turso** (libSQL, all structured data), **Cloudflare R2** +to three backends: **Cloudflare D1** (all structured data), **Cloudflare R2** (PDF binaries), and an **AI gateway** (Workers AI, with BYOK and a local-dev bridge as alternatives). @@ -45,8 +45,8 @@ Every claim below is grounded in the file named next to it. with Google OAuth for the webapp, plus an `rdr_*` API-key path for the Chrome extension. `getAuthenticatedUserId()` is the single choke point every protected handler calls first. -- **Data layer** (`src/lib/db/`, `src/lib/*-db.ts`) — Drizzle over a Turso - libSQL client, wrapped in `*-db.ts` helpers (`articles-db.ts`, `lists-db.ts`, +- **Data layer** (`src/lib/db/`, `src/lib/*-db.ts`) — Drizzle over the Worker's + D1 binding, wrapped in `*-db.ts` helpers (`articles-db.ts`, `lists-db.ts`, …) that own queries, ownership scoping, and edge-cache invalidation. - **PDF storage** (`src/lib/storage.ts`, `src/worker/routes/pdf.ts`) — binaries live in the R2 `PDFS_BUCKET`; the DB stores only a `blob://` reference, @@ -72,7 +72,7 @@ flowchart TD SNAP --> SF["validateExternalUrl + safe-fetch
(SSRF boundary)"] SF --> READ["Readability + linkedom
extract clean article"] READ --> SAVE["POST /api/articles
createArticleRecord (articles-db.ts)"] - SAVE --> DB[(Turso libSQL
articles, scoped by userId)] + SAVE --> DB[(Cloudflare D1
articles, scoped by userId)] U2[User opens reader/:id] --> GET["GET /api/articles/:id"] GET --> CACHE{"caches.default
5-min TTL?"} CACHE -->|hit| RENDER[ReaderView renders] @@ -107,7 +107,7 @@ flowchart TD 3. **Read.** Opening `reader/:id` calls `GET /api/articles/:id`. Reads are served from Cloudflare's `caches.default` with a 5-minute TTL when available, - falling back to Turso; writes explicitly bust that cache. `ReaderView` renders + falling back to D1; writes explicitly bust that cache. `ReaderView` renders the sanitised HTML with the user's theme/font/size preferences. 4. **Annotate.** Selecting text surfaces *Add note* / *Ask AI*. A note is a @@ -134,11 +134,11 @@ mental model. server rendering to justify SSR. A static SPA served from one Worker is cheaper, simpler to reason about, and avoids the OpenNext build complexity the project ran into. → [0001-vite-spa-hono-worker.md](decisions/0001-vite-spa-hono-worker.md). -- **Turso + Drizzle for structured data.** A SQLite-compatible edge database +- **D1 + Drizzle for structured data.** A SQLite-compatible edge database keeps latency low near the Worker while giving typed, migratable schema. JSON-in-text columns (`notes`, `aiChat`, `summary`, …) avoid a table sprawl for data that is always read with its parent article. → - [0002-turso-drizzle.md](decisions/0002-turso-drizzle.md). + [0010-cloudflare-d1.md](decisions/0010-cloudflare-d1.md). - **R2 for PDFs, DB stores only a reference.** Large binaries do not belong in a row-oriented DB; R2 is cheap object storage bound directly to the Worker, and proxied downloads keep the bucket private. → diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 2528e8e..b7a7b07 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -27,12 +27,12 @@ Vite entry) and routes client-side via `react-router-dom`; the Worker handles │ │ data-export, ext chat, browser-mem)│ │ └── env.ASSETS.fetch() ← built SPA + landing (dist/) │ │ │ - │ Bindings: PDFS_BUCKET (R2), ASSETS (dist/) │ + │ Bindings: DB (D1), PDFS_BUCKET (R2), ASSETS (dist/) │ └────────────────────────────────┬─────────────────────────────────────────┘ │ ┌──────────────────────────────┼──────────────────────────────┐ ▼ ▼ ▼ - Turso (libSQL) Cloudflare R2 free-ai-gateway + Cloudflare D1 Cloudflare R2 free-ai-gateway via Drizzle ORM reader-pdfs bucket (AI_BASE_URL) (articles, boards, (PDF binaries, + BYOK providers lists, memories, rss, proxied downloads) + local-ai (dev) @@ -47,7 +47,7 @@ Vite entry) and routes client-side via `react-router-dom`; the Worker handles `/api/`. - `src/lib/db/schema.ts` — Drizzle schema (app tables + better-auth tables + legacy NextAuth tables kept for reference). -- `src/lib/db/client.ts` — Turso libSQL client. Lazy proxy so the client is +- `src/lib/db/client.ts` — D1 Drizzle client. Lazy proxy so the client is not created at module load (required for the Workers runtime). - `src/lib/auth.ts` — better-auth server config (`createAuth`), Drizzle adapter, Google OAuth, `oneTap` plugin, rate limiting disabled. @@ -59,7 +59,7 @@ Vite entry) and routes client-side via `react-router-dom`; the Worker handles - `src/lib/url-validation.ts` + `src/lib/safe-fetch.ts` — SSRF protection and redirect-safe fetch used by snapshot/proxy/RSS refresh. - `src/router.tsx` — client-side routes (lazy-loaded pages). -- `wrangler.toml` — Worker config: `main = src/worker.ts`, `ASSETS` + `PDFS_BUCKET` +- `wrangler.toml` — Worker config: `main = src/worker.ts`, `DB` + `ASSETS` + `PDFS_BUCKET` bindings, `placement.mode = "smart"`, `nodejs_compat_v2`, custom domain. - `vite.config.ts` — Vite SPA build (React, Tailwind v4, Lightning CSS). - `app.html` — single SPA HTML entry (Vite input; carries inline shell CSS). diff --git a/docs/development/commands.md b/docs/development/commands.md index 53d1aee..c523fe7 100644 --- a/docs/development/commands.md +++ b/docs/development/commands.md @@ -26,9 +26,10 @@ ordering; run `pnpm run` to see the live list. | `pnpm test:coverage` | `vitest run --coverage` | | `pnpm test:e2e` | `playwright test` | | `pnpm memory:demo` | `tsx scripts/memory-capture-demo.ts` | -| `pnpm db:push` | `drizzle-kit push` (schema sync) | -| `pnpm db:studio` | `drizzle-kit studio` | -| `pnpm migrate:firestore` | Legacy Firestore → Turso migration (`tsx scripts/migrate-firestore-to-turso.ts`) | +| `pnpm db:generate` | Generate a tracked D1 migration from `src/lib/db/schema.ts` | +| `pnpm db:migrate:local` | Apply tracked migrations to isolated local D1 | +| `pnpm db:migrate:remote` | Explicitly apply tracked migrations to the configured remote D1 | +| `pnpm migrate:firestore` | Legacy Firestore → D1 migration (`tsx scripts/migrate-firestore-to-d1.ts`; dry-run by default) | | `pnpm prepare` | `husky` (installs pre-commit hook) | | `pnpm format` | `biome format --write .` | | `pnpm format:check` | `biome format .` | diff --git a/docs/development/setup.md b/docs/development/setup.md index 1610b72..c8a7a83 100644 --- a/docs/development/setup.md +++ b/docs/development/setup.md @@ -4,7 +4,7 @@ - Node.js 22+ (engines field in `package.json`; CI uses 24). - pnpm 10+ (the `packageManager` field pins the exact version). -- A Turso database (`turso db create`) and auth token. +- Wrangler for the isolated local D1 database (already installed by `pnpm install`). - A Cloudflare account with an R2 bucket `reader-pdfs` bound as `PDFS_BUCKET`. - A Google OAuth client (Google Cloud Console → APIs & Services → Credentials) with a redirect URI for `BETTER_AUTH_URL` (e.g. @@ -25,7 +25,6 @@ cp .env.example .env.local Edit `.env.local` (see [operations/env.md](../operations/env.md) for the full list and validation): -- `TURSO_DATABASE_URL`, `TURSO_AUTH_TOKEN` - `BETTER_AUTH_SECRET` (`openssl rand -base64 32`), `BETTER_AUTH_URL` - `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` - `AI_GATEWAY_API_KEY` (free-ai gateway) — optional for BYOK-only dev @@ -34,10 +33,10 @@ list and validation): R2 credentials are only needed for production / `wrangler dev`; the binding itself is provided by `wrangler dev` from `wrangler.toml`. -## Push the schema +## Apply the schema ```bash -pnpm db:push # drizzle-kit push → applies schema to Turso +pnpm db:migrate:local # tracked migrations → isolated local D1 ``` See [operations/runbooks/migrate-schema.md](../operations/runbooks/migrate-schema.md) diff --git a/docs/index.md b/docs/index.md index 87b6c94..1f6dbb8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -51,7 +51,7 @@ docs/ jobs.md # scheduled jobs (weekly quality check) runbooks/ migrate-schema.md # applying Drizzle schema changes - rotate-secrets.md # rotating Cloudflare/Turso/Google secrets + rotate-secrets.md # rotating Cloudflare and Google secrets rollback.md # rollback procedure for a bad deploy knowledge/ learnings.md # current, applicable engineering lessons diff --git a/docs/marketing/iterations/v2/create-objection-handling-faq.md b/docs/marketing/iterations/v2/create-objection-handling-faq.md index 72a78ba..fa03053 100644 --- a/docs/marketing/iterations/v2/create-objection-handling-faq.md +++ b/docs/marketing/iterations/v2/create-objection-handling-faq.md @@ -45,7 +45,7 @@ It depends on how you read. If you're reading something technical or research-he Your annotations are private by default. Nothing is shared unless you explicitly create a public share link for an article. The share link exposes the article text but does not expose your private highlights or notes. AI features run on the article text; your notes are included only when you actively open the AI chat panel for that article. -**Honest limit:** Reader is a cloud-based app — your data lives on Turso (a managed database) and Cloudflare infrastructure. If that's a dealbreaker for sensitive research, local-first tools like Obsidian are the right fit. +**Honest limit:** Reader is a cloud-based app — your data lives on Cloudflare infrastructure. If that's a dealbreaker for sensitive research, local-first tools like Obsidian are the right fit. --- diff --git a/docs/operations/ci-cd.md b/docs/operations/ci-cd.md index e68be8a..c88c522 100644 --- a/docs/operations/ci-cd.md +++ b/docs/operations/ci-cd.md @@ -30,8 +30,7 @@ exists) → `cf:build` → `cloudflare/wrangler-action@v3 deploy` → smoke chec **Required GitHub secrets:** `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`. **Required Cloudflare Worker secrets:** `BETTER_AUTH_SECRET`, -`GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `TURSO_AUTH_TOKEN`, -`TURSO_DATABASE_URL`. +`GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`. See [deploy.md](deploy.md) for the full pipeline. diff --git a/docs/operations/deploy.md b/docs/operations/deploy.md index 96b8ab4..3e9e03e 100644 --- a/docs/operations/deploy.md +++ b/docs/operations/deploy.md @@ -23,10 +23,11 @@ the `ASSETS` binding. `wrangler.toml` configures: - `compatibility_date = "2025-04-01"`, `compatibility_flags = ["nodejs_compat_v2"]` - `assets = { directory = "dist", binding = "ASSETS", run_worker_first = ["/sitemap.xml", "/index.md", "/llms-full.txt", "/llms.txt", "/api/*", "/"] }` - `routes = [{ pattern = "read.significanthobbies.com", custom_domain = true }]` -- `[placement] mode = "smart"` (co-locate Worker with Turso) +- `[placement] mode = "smart"` - `[observability] enabled = true, head_sampling_rate = 0.1` - `[limits] cpu_ms = 30000` - `[[r2_buckets]] binding = "PDFS_BUCKET", bucket_name = "reader-pdfs"` +- `[[d1_databases]] binding = "DB"` after the approved production cutover - `[vars] AI_BASE_URL`, `BETTER_AUTH_URL`, `NODE_ENV = "production"` ## Required Cloudflare secrets @@ -36,8 +37,6 @@ Set via `wrangler secret put `: - `BETTER_AUTH_SECRET` - `GOOGLE_CLIENT_ID` - `GOOGLE_CLIENT_SECRET` -- `TURSO_AUTH_TOKEN` -- `TURSO_DATABASE_URL` - `AI_GATEWAY_API_KEY` (free-ai gateway; legacy alias `AI_API_KEY`) The deploy workflow validates that each required secret exists in diff --git a/docs/operations/env.md b/docs/operations/env.md index 8cc2b8b..f32d952 100644 --- a/docs/operations/env.md +++ b/docs/operations/env.md @@ -8,8 +8,7 @@ for that mode is missing or empty. | Variable | Required where | Set via | Purpose | | --- | --- | --- | --- | -| `TURSO_DATABASE_URL` | runtime, deploy | Wrangler secret | Turso libSQL URL (`libsql://...`) | -| `TURSO_AUTH_TOKEN` | runtime, deploy | Wrangler secret | Turso auth token | +| `DB` | runtime | `wrangler.toml` binding | Cloudflare D1 application and auth database | | `BETTER_AUTH_SECRET` | runtime, deploy | Wrangler secret | better-auth session signing key (`openssl rand -base64 32`) | | `BETTER_AUTH_URL` | — | `wrangler.toml [vars]` | OAuth callback base URL (prod: `https://read.significanthobbies.com`) | | `GOOGLE_CLIENT_ID` | runtime, deploy | Wrangler secret | Google OAuth client ID | @@ -33,16 +32,15 @@ for that mode is missing or empty. ## Validation modes - `build` — no required vars (the build does not need runtime secrets). -- `runtime` — `BETTER_AUTH_SECRET`, `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, - `TURSO_AUTH_TOKEN`, `TURSO_DATABASE_URL`. +- `runtime` — `BETTER_AUTH_SECRET`, `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`. - `deploy` — same as `runtime`. ## Local dev -`.env.local` is loaded by `drizzle.config.ts` and `vite.config.ts` via -`dotenv/config`. `.env.example` is the committed template; `.env.local` is -gitignored. The Worker dev server (`pnpm dev:worker`) reads Wrangler secrets -from `.dev.vars` (gitignored) and bindings from `wrangler.toml`. +`.env.local` is loaded by `vite.config.ts` via `dotenv/config`. `.env.example` +is the committed template; `.env.local` is gitignored. The Worker dev server +(`pnpm dev:worker`) reads Wrangler secrets from `.dev.vars` (gitignored) and +isolated D1/R2 bindings from `wrangler.local.toml`. ## Security diff --git a/docs/operations/runbooks/migrate-schema.md b/docs/operations/runbooks/migrate-schema.md index f5581ee..f3145cc 100644 --- a/docs/operations/runbooks/migrate-schema.md +++ b/docs/operations/runbooks/migrate-schema.md @@ -1,9 +1,8 @@ # Runbook: Apply a Drizzle Schema Change -Reader uses `drizzle-kit push` (`pnpm db:push`) for schema sync, with additive -migration SQL files committed under `drizzle/` for deliberate application in -production. The migration journal is `drizzle/meta/_journal.json` with -snapshots in `drizzle/meta/`. +Reader uses generated, additive migration SQL committed under `drizzle/` and +applied through Wrangler's D1 migration ledger. The Drizzle journal is +`drizzle/meta/_journal.json` with snapshots in `drizzle/meta/`. ## When to use this runbook @@ -14,12 +13,12 @@ snapshots in `drizzle/meta/`. ## Local / dev ```bash -pnpm db:push # drizzle-kit push → diffs schema.ts against the live DB and applies +pnpm db:generate # after editing the schema +pnpm db:migrate:local # applies only to isolated local D1 ``` -`drizzle.config.ts` loads `.env.local` for `TURSO_DATABASE_URL` and -`TURSO_AUTH_TOKEN`. Push is schema-sync (no migration history); safe for a -single-user DB. +`drizzle.config.ts` generates SQLite-compatible D1 migrations without database +credentials. `wrangler.local.toml` owns the isolated local binding. ## Production @@ -30,18 +29,14 @@ single-user DB. it. For additive migrations the order is: migrate → deploy. For destructive migrations, deploy backward-compatible code first, then migrate, then remove the old code path. -3. **Apply via Turso** (not `wrangler`): +3. **Apply via Wrangler only after explicit operator approval:** ```bash - # Option A: drizzle-kit push against the production DB - TURSO_DATABASE_URL= TURSO_AUTH_TOKEN= pnpm db:push - - # Option B: run the SQL file directly with the Turso CLI - turso db shell < drizzle/0002_first_green_goblin.sql + pnpm db:migrate:remote ``` -4. **Verify** with `pnpm db:studio` (read-only inspection) or a direct Turso - query (`turso db shell ".tables"`). +4. **Verify** with an explicitly remote, read-only Wrangler D1 query and the + migration receipt checks. 5. **Deploy** the application code per [deploy.md](../deploy.md). ## Example: the RSS migration @@ -54,15 +49,10 @@ the RSS routes. See [architecture/decisions/0008-rss-inbox.md](../../architectur - **Additive migration:** drop the new table/column/index. Data in the new table is disposable (e.g. RSS entries are transient inbox items). -- **Destructive migration:** restore from Turso backup (`turso db shell - ".restore "`). Take a backup before any destructive - change. +- **Destructive migration:** restore through the separately rehearsed D1 + recovery plan. Take an export before any destructive change. ## Discipline - Prefer additive migrations (new tables, new nullable columns, new indexes). -- Avoid `drizzle-kit push` against production with data in an incompatible old - shape — read the generated SQL first. -- Open question (tracked in STATUS.md): switch from `drizzle-kit push` to - `drizzle-kit generate` for safer schema changes as user count grows. See - [knowledge/learnings.md](../../knowledge/learnings.md). +- Never use schema push against production; read generated SQL first. diff --git a/docs/operations/runbooks/rollback.md b/docs/operations/runbooks/rollback.md index fef88ed..49d9010 100644 --- a/docs/operations/runbooks/rollback.md +++ b/docs/operations/runbooks/rollback.md @@ -30,17 +30,13 @@ case) do not require rollback — old code ignores new tables/columns. For a destructive migration that removed data: -1. Restore the Turso DB from the most recent backup: +1. Restore D1 through the rehearsed Cloudflare recovery procedure recorded in + the migration receipt. During the Turso-to-D1 observation window, the + preserved source database is the separate rollback authority. +2. Redeploy the matching known-good application code (above). +3. Verify with read-only D1 integrity and critical-journey checks. - ```bash - turso db shell ".databases" # confirm the DB - turso db shell ".restore " - ``` - -2. Redeploy the last known-good application code (above). -3. Verify with `pnpm db:studio` or a direct query. - -Take a Turso backup before any destructive migration in the future. +Take a D1 export before any destructive migration in the future. ## Rollback a landing overlay change @@ -66,7 +62,7 @@ in place on `main` without a smoke check. - **R2 object deletes** — if a deploy deleted PDF objects, they are gone unless you have R2 replication or a bucket backup. Avoid `delete` calls in new code without a soft-delete path. -- **Turso row deletes** — restore from backup. +- **D1 row deletes** — restore through the rehearsed recovery procedure. - **OAuth session invalidation** — rotating `BETTER_AUTH_SECRET` invalidates all sessions; users must sign in again. There is no rollback. diff --git a/docs/operations/runbooks/rotate-secrets.md b/docs/operations/runbooks/rotate-secrets.md index 257268f..fe27467 100644 --- a/docs/operations/runbooks/rotate-secrets.md +++ b/docs/operations/runbooks/rotate-secrets.md @@ -2,13 +2,13 @@ Rotate on a schedule, after a suspected leak, or when personnel changes. The production Worker reads secrets from Cloudflare Workers secrets -(`wrangler secret list`); Turso and Google OAuth have their own consoles. +(`wrangler secret list`); Google OAuth has its own console. D1 and R2 use +native bindings rather than database credentials in the Worker. ## Cloudflare Worker secrets ```bash wrangler secret put BETTER_AUTH_SECRET # openssl rand -base64 32 -wrangler secret put TURSO_AUTH_TOKEN # from Turso console wrangler secret put GOOGLE_CLIENT_SECRET # from Google Cloud Console wrangler secret put AI_GATEWAY_API_KEY # from free-ai gateway ``` @@ -22,12 +22,6 @@ After rotating `BETTER_AUTH_SECRET`, existing sessions are invalidated — users must sign in again. There is only one production user today, so this is a non-event. -## Turso - -1. Turso console → database → Tokens → create a new token. -2. `wrangler secret put TURSO_AUTH_TOKEN` with the new value. -3. Revoke the old token in the Turso console once the new one is live. - ## Google OAuth 1. Google Cloud Console → APIs & Services → Credentials → your OAuth client. diff --git a/docs/product/features.md b/docs/product/features.md index b179ac2..f716e70 100644 --- a/docs/product/features.md +++ b/docs/product/features.md @@ -62,7 +62,7 @@ update when behaviour changes. For the API route map, see ## Memory capture -- `/memory` page with persisted, authenticated captures (`memories` Turso +- `/memory` page with persisted, authenticated captures (`memories` D1 table, `/api/memories` CRUD + `/api/memories/search`). - `POST /api/browser-memory/import` for browser-memory imports. - Global SearchBar routes memory hits to `/memory`. diff --git a/docs/product/overview.md b/docs/product/overview.md index 94cf425..f3efb28 100644 --- a/docs/product/overview.md +++ b/docs/product/overview.md @@ -12,7 +12,7 @@ browser and syncs with Chrome's native Reading List. - **End users:** individual readers saving articles and PDFs. Sign-in is Google OAuth via better-auth; data is per-user isolated at the database level. -- **Operators:** the maintainer running Turso schema migrations and Cloudflare +- **Operators:** the maintainer running D1 schema migrations and Cloudflare Workers deploys. Currently single-user in production. ## Where @@ -27,7 +27,7 @@ browser and syncs with Chrome's native Reading List. **In scope:** article/PDF capture, rich annotations, tags/lists/boards, full-text search, AI chat and summaries, RSS/Atom inbox with OPML import, -memory capture, Turso persistence, R2 PDF storage, free-ai gateway + BYOK + +memory capture, D1 persistence, R2 PDF storage, free-ai gateway + BYOK + local-ai dev bridge. **Out of scope (deliberate):** diff --git a/drizzle.config.ts b/drizzle.config.ts index 7c381f9..545df80 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,20 +1,9 @@ -import { config } from 'dotenv'; import { defineConfig } from 'drizzle-kit'; -config({ path: '.env.local' }); - -const url = process.env.TURSO_DATABASE_URL; -const authToken = process.env.TURSO_AUTH_TOKEN; - -if (!url || !authToken) { - throw new Error('TURSO_DATABASE_URL and TURSO_AUTH_TOKEN must be set in .env.local'); -} - export default defineConfig({ - dialect: 'turso', + dialect: 'sqlite', schema: './src/lib/db/schema.ts', out: './drizzle', - dbCredentials: { url, authToken }, verbose: true, strict: true, }); diff --git a/package.json b/package.json index 4bf1de8..ec3216b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "concurrently -k -n worker,spa,ai -c blue,green,magenta \"pnpm dev:worker\" \"pnpm dev:spa\" \"pnpm local-ai\"", "dev:spa": "vite", - "dev:worker": "wrangler dev", + "dev:worker": "wrangler dev --config wrangler.local.toml", "local-ai": "node scripts/local-ai.mjs", "cli-bridge": "pnpm local-ai", "build": "node scripts/validate-env.mjs build && vite build", @@ -24,9 +24,10 @@ "test:coverage": "vitest run --coverage", "test:e2e": "playwright test", "memory:demo": "tsx scripts/memory-capture-demo.ts", - "db:push": "drizzle-kit push", - "db:studio": "drizzle-kit studio", - "migrate:firestore": "tsx scripts/migrate-firestore-to-turso.ts", + "db:generate": "drizzle-kit generate", + "db:migrate:local": "wrangler d1 migrations apply DB --local --config wrangler.local.toml", + "db:migrate:remote": "wrangler d1 migrations apply DB --remote", + "migrate:firestore": "tsx scripts/migrate-firestore-to-d1.ts", "prepare": "husky", "format": "biome format --write .", "format:check": "biome format .", @@ -49,7 +50,6 @@ "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/react": "3.0.86", - "@libsql/client": "0.17.4", "@mozilla/readability": "0.6.0", "@radix-ui/react-dialog": "1.1.23", "@radix-ui/react-dropdown-menu": "2.1.24", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6fecbd4..0da8c74 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,9 +17,6 @@ importers: '@ai-sdk/react': specifier: 3.0.86 version: 3.0.86(react@19.2.8)(zod@4.4.3) - '@libsql/client': - specifier: 0.17.4 - version: 0.17.4 '@mozilla/readability': specifier: 0.6.0 version: 0.6.0 @@ -6669,10 +6666,12 @@ snapshots: transitivePeerDependencies: - bufferutil - utf-8-validate + optional: true '@libsql/core@0.17.4': dependencies: js-base64: 3.9.1 + optional: true '@libsql/darwin-arm64@0.5.29': optional: true @@ -6687,6 +6686,7 @@ snapshots: transitivePeerDependencies: - bufferutil - utf-8-validate + optional: true '@libsql/isomorphic-ws@0.1.5': dependencies: @@ -6695,6 +6695,7 @@ snapshots: transitivePeerDependencies: - bufferutil - utf-8-validate + optional: true '@libsql/linux-arm-gnueabihf@0.5.29': optional: true @@ -6836,7 +6837,8 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true - '@neon-rs/load@0.0.4': {} + '@neon-rs/load@0.0.4': + optional: true '@next/env@16.2.4': optional: true @@ -8428,7 +8430,8 @@ snapshots: destr@2.0.5: {} - detect-libc@2.0.2: {} + detect-libc@2.0.2: + optional: true detect-libc@2.1.2: {} @@ -8951,7 +8954,8 @@ snapshots: jose@6.2.4: {} - js-base64@3.9.1: {} + js-base64@3.9.1: + optional: true js-tokens@4.0.0: {} @@ -9001,6 +9005,7 @@ snapshots: '@libsql/linux-x64-gnu': 0.5.29 '@libsql/linux-x64-musl': 0.5.29 '@libsql/win32-x64-msvc': 0.5.29 + optional: true lightningcss-android-arm64@1.32.0: optional: true @@ -9800,7 +9805,8 @@ snapshots: prismjs@1.30.0: {} - promise-limit@2.7.0: {} + promise-limit@2.7.0: + optional: true prompts@2.4.2: dependencies: diff --git a/scripts/migrate-firestore-to-turso.ts b/scripts/migrate-firestore-to-d1.ts similarity index 92% rename from scripts/migrate-firestore-to-turso.ts rename to scripts/migrate-firestore-to-d1.ts index c0f9ce2..39db2cd 100644 --- a/scripts/migrate-firestore-to-turso.ts +++ b/scripts/migrate-firestore-to-d1.ts @@ -1,15 +1,16 @@ /** - * One-shot Firestore → Turso migration script. + * One-shot Firestore → D1 migration script. * * Usage: * pnpm migrate:firestore # dry-run (default, safe) - * pnpm migrate:firestore --apply # actually write to Turso + * pnpm migrate:firestore --apply # write to local D1 + * pnpm migrate:firestore --apply --remote # explicitly write to remote D1 * * What it does (in order): - * 1. users — Firebase Auth → Turso `user` table (one row for the target uid) - * 2. lists — Firestore `lists` → Turso `lists` - * 3. articles — Firestore `annotations` → Turso `articles` - * 4. boards — Firestore `boards` → Turso `boards` + * 1. users — Firebase Auth → D1 `user` table (one row for the target uid) + * 2. lists — Firestore `lists` → D1 `lists` + * 3. articles — Firestore `annotations` → D1 `articles` + * 4. boards — Firestore `boards` → D1 `boards` * * Idempotency: every insert uses INSERT ... ON CONFLICT(id) DO UPDATE (upsert). * A re-run of --apply is safe; it refreshes column values. @@ -25,11 +26,11 @@ import { resolve } from 'node:path'; import { dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { createClient } from '@libsql/client'; import { config as loadEnv } from 'dotenv'; import { sql } from 'drizzle-orm'; -import { drizzle } from 'drizzle-orm/libsql'; +import { drizzle } from 'drizzle-orm/d1'; import admin from 'firebase-admin'; +import { getPlatformProxy } from 'wrangler'; // Relative imports (scripts run outside Next.js, so no `@/` alias). import * as schema from '../src/lib/db/schema'; @@ -44,8 +45,14 @@ loadEnv({ path: resolve(repoRoot, '.env.local') }); // --- CLI flags --- const args = process.argv.slice(2); const APPLY = args.includes('--apply'); +const REMOTE = args.includes('--remote'); const DRY_RUN = !APPLY; +if (REMOTE && !APPLY) { + console.error('--remote requires --apply so remote access is always explicit.'); + process.exit(1); +} + // Single-user migration target (per plan). const TARGET_USER_ID = 'zxhdLO9NLwcTgRJNLNNFHTiAEnH3'; @@ -68,15 +75,12 @@ if (!admin.apps.length) { const firestore = admin.firestore(); const auth = admin.auth(); -// --- Turso init (mirrors src/lib/db/client.ts but without the Next-time throw) --- -const url = process.env.TURSO_DATABASE_URL; -const authToken = process.env.TURSO_AUTH_TOKEN; -if (!url || !authToken) { - console.error('TURSO_DATABASE_URL and TURSO_AUTH_TOKEN must be set in .env.local'); - process.exit(1); -} -const libsql = createClient({ url, authToken }); -const db = drizzle(libsql, { schema }); +// --- D1 init --- +const platform = await getPlatformProxy<{ DB: Parameters[0] }>({ + configPath: REMOTE ? './wrangler.toml' : './wrangler.local.toml', + remoteBindings: REMOTE, +}); +const db = drizzle(platform.env.DB, { schema }); // --- Helpers --- type Counts = { fetched: number; inserted: number; skipped: number; errored: number }; @@ -263,7 +267,7 @@ async function migrateArticles(counts: Counts): Promise<{ pdfCount: number }> { const data = doc.data() ?? {}; const id = doc.id; - // projectId is dropped in Turso schema (see plan §2.5) — don't carry it. + // projectId is absent from the current schema (see plan §2.5) — don't carry it. const pdfMetadata = data.pdfMetadata ?? null; const pdfStorageKey = pdfMetadata && @@ -445,9 +449,9 @@ async function migrateBoards(counts: Counts): Promise { // --- Main --- async function main(): Promise { console.log('─'.repeat(60)); - console.log(`Firestore → Turso migration (${DRY_RUN ? 'DRY-RUN' : 'APPLY'})`); + console.log(`Firestore → D1 migration (${DRY_RUN ? 'DRY-RUN' : 'APPLY'})`); console.log(`Target user: ${TARGET_USER_ID}`); - console.log(`Turso URL: ${url}`); + console.log(`Target: ${REMOTE ? 'remote D1' : 'local D1'}`); console.log('─'.repeat(60)); const totals = { @@ -481,12 +485,11 @@ async function main(): Promise { console.log('\n Re-run with --apply to write.\n'); } - // libsql client keeps the event loop alive; close explicitly. - libsql.close(); + await platform.dispose(); } main().catch((error) => { console.error('\nMigration failed:', error); - libsql.close(); + void platform.dispose(); process.exit(1); }); diff --git a/scripts/validate-env.mjs b/scripts/validate-env.mjs index 286c65a..3fa13ec 100644 --- a/scripts/validate-env.mjs +++ b/scripts/validate-env.mjs @@ -4,20 +4,8 @@ const mode = process.argv[2] ?? 'runtime'; const requiredByMode = { build: [], - runtime: [ - 'BETTER_AUTH_SECRET', - 'GOOGLE_CLIENT_ID', - 'GOOGLE_CLIENT_SECRET', - 'TURSO_AUTH_TOKEN', - 'TURSO_DATABASE_URL', - ], - deploy: [ - 'BETTER_AUTH_SECRET', - 'GOOGLE_CLIENT_ID', - 'GOOGLE_CLIENT_SECRET', - 'TURSO_AUTH_TOKEN', - 'TURSO_DATABASE_URL', - ], + runtime: ['BETTER_AUTH_SECRET', 'GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET'], + deploy: ['BETTER_AUTH_SECRET', 'GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET'], }; const required = requiredByMode[mode]; diff --git a/src/lib/__tests__/browser-memory-import.test.ts b/src/lib/__tests__/browser-memory-import.test.ts index 0758531..ee906e3 100644 --- a/src/lib/__tests__/browser-memory-import.test.ts +++ b/src/lib/__tests__/browser-memory-import.test.ts @@ -38,7 +38,7 @@ vi.mock('../articles-db', () => ({ })); // Memories are persisted via memories-db (not articles-db). Mock it so the -// import flow can be exercised without a live Turso connection. +// import flow can be exercised without a live D1 binding. vi.mock('../memories-db', () => ({ findMemoryByUrl: vi.fn().mockResolvedValue(null), createMemoryRecord: vi.fn().mockResolvedValue('memory-id-1'), diff --git a/src/lib/articles-db.ts b/src/lib/articles-db.ts index e75d60e..8869a11 100644 --- a/src/lib/articles-db.ts +++ b/src/lib/articles-db.ts @@ -358,7 +358,7 @@ export async function fetchArticleSummaries( projectId?: string, listId?: string ): Promise { - // projectId retained for signature parity; the Turso schema drops the column. + // projectId is retained for signature parity; the current schema omits the column. void projectId; try { const rows = await db @@ -721,7 +721,7 @@ export async function searchArticles( projectId?: string ): Promise { // projectId is retained for signature parity with articles-service; the - // Turso schema drops the column, so filtering is a no-op here. + // The current schema omits the column, so filtering is a no-op here. void projectId; const sanitizedQuery = sanitizePlainText(query); if (!sanitizedQuery || sanitizedQuery.length < 2) return []; diff --git a/src/lib/db/client.ts b/src/lib/db/client.ts index 67c6790..589a058 100644 --- a/src/lib/db/client.ts +++ b/src/lib/db/client.ts @@ -1,21 +1,13 @@ -import { createClient } from '@libsql/client/web'; -import { drizzle } from 'drizzle-orm/libsql'; +import { drizzle } from 'drizzle-orm/d1'; import * as schema from './schema'; export type DbEnv = { - TURSO_DATABASE_URL: string; - TURSO_AUTH_TOKEN?: string; + DB: Parameters[0]; }; export function createDb(env: DbEnv) { - const url = env.TURSO_DATABASE_URL; - const httpUrl = url.replace(/^libsql:\/\//, 'https://'); - const client = createClient({ - url: httpUrl, - authToken: env.TURSO_AUTH_TOKEN, - }); - return drizzle(client, { schema }); + return drizzle(env.DB, { schema }); } type Database = ReturnType; @@ -23,19 +15,7 @@ type Database = ReturnType; let cachedDb: Database | undefined; function getDb(): Database { - if (cachedDb) return cachedDb; - - const url = process.env.TURSO_DATABASE_URL; - const authToken = process.env.TURSO_AUTH_TOKEN; - - if (!url) { - throw new Error('TURSO_DATABASE_URL is not set. Add it to .env.local.'); - } - if (!authToken) { - throw new Error('TURSO_AUTH_TOKEN is not set. Add it to .env.local.'); - } - - cachedDb = createDb({ TURSO_DATABASE_URL: url, TURSO_AUTH_TOKEN: authToken }); + if (!cachedDb) throw new Error('D1 database binding has not been initialized for this request.'); return cachedDb; } diff --git a/src/lib/worker-env.ts b/src/lib/worker-env.ts index 7a93dfa..789179b 100644 --- a/src/lib/worker-env.ts +++ b/src/lib/worker-env.ts @@ -1,6 +1,5 @@ export type WorkerEnv = { - TURSO_DATABASE_URL: string; - TURSO_AUTH_TOKEN?: string; + DB: D1Database; BETTER_AUTH_SECRET?: string; AUTH_SECRET?: string; BETTER_AUTH_URL?: string; diff --git a/src/pages/AboutPage.tsx b/src/pages/AboutPage.tsx index cb6df60..4256d3d 100644 --- a/src/pages/AboutPage.tsx +++ b/src/pages/AboutPage.tsx @@ -35,8 +35,8 @@ export default function AboutPage() {

Stack

- Vite + React 19 SPA, Hono worker on Cloudflare, Turso (libSQL) + Drizzle, better-auth - (Google), Cloudflare R2 for PDF storage, Vercel AI SDK for chat. Chrome MV3 extension builds + Vite + React 19 SPA, Hono worker on Cloudflare, D1 + Drizzle, better-auth (Google), + Cloudflare R2 for PDF storage, Vercel AI SDK for chat. Chrome MV3 extension builds independently in packages/chrome-extension/.

diff --git a/src/worker/bind-env.ts b/src/worker/bind-env.ts index 1f657eb..d8bd692 100644 --- a/src/worker/bind-env.ts +++ b/src/worker/bind-env.ts @@ -3,8 +3,6 @@ import { setPdfBucket } from '../lib/storage'; import type { WorkerEnv } from '../lib/worker-env'; const STRING_KEYS: (keyof WorkerEnv)[] = [ - 'TURSO_DATABASE_URL', - 'TURSO_AUTH_TOKEN', 'BETTER_AUTH_SECRET', 'AUTH_SECRET', 'BETTER_AUTH_URL', @@ -27,9 +25,7 @@ export function bindWorkerEnv(env: WorkerEnv) { } } - if (env.TURSO_DATABASE_URL) { - setDb(createDb(env)); - } + setDb(createDb(env)); if (env.PDFS_BUCKET) { setPdfBucket(env.PDFS_BUCKET); diff --git a/tsconfig.app.json b/tsconfig.app.json index 824115c..d3599d2 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -20,6 +20,7 @@ "esModuleInterop": true, "isolatedModules": true, "jsx": "react-jsx", + "types": ["node"], "paths": { "@/*": ["./src/*"] } }, "include": [ diff --git a/tsconfig.worker.json b/tsconfig.worker.json index 484fe48..43464ea 100644 --- a/tsconfig.worker.json +++ b/tsconfig.worker.json @@ -20,7 +20,7 @@ "esModuleInterop": true, "isolatedModules": true, "paths": { "@/*": ["./src/*"] }, - "types": ["@cloudflare/workers-types"] + "types": ["@cloudflare/workers-types", "node"] }, "include": [ "src/worker.ts", diff --git a/wrangler.local.toml b/wrangler.local.toml new file mode 100644 index 0000000..54072c0 --- /dev/null +++ b/wrangler.local.toml @@ -0,0 +1,21 @@ +name = "reader-local" +main = "src/worker.ts" +compatibility_date = "2025-04-01" +compatibility_flags = ["nodejs_compat_v2"] + +assets = { directory = "dist", binding = "ASSETS", run_worker_first = ["/sitemap.xml", "/index.md", "/llms-full.txt", "/llms.txt", "/api/*", "/"] } + +[vars] +NODE_ENV = "development" +AI_BASE_URL = "http://127.0.0.1:11435/v1" +BETTER_AUTH_URL = "http://localhost:8787" + +[[d1_databases]] +binding = "DB" +database_name = "reader-local" +database_id = "local" +migrations_dir = "drizzle" + +[[r2_buckets]] +binding = "PDFS_BUCKET" +bucket_name = "reader-pdfs-local" diff --git a/wrangler.toml b/wrangler.toml index 28ddb11..b3bedb3 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -31,3 +31,10 @@ BETTER_AUTH_URL = "https://read.significanthobbies.com" [[r2_buckets]] binding = "PDFS_BUCKET" bucket_name = "reader-pdfs" + +[[d1_databases]] +binding = "DB" +database_name = "reader" +database_id = "f59f66af-79d1-4c2d-be34-fbdf20ed8da6" +preview_database_id = "fb70ecb4-ff51-413f-9b21-d87456a97a9d" +migrations_dir = "drizzle"