Skip to content

Latest commit

 

History

History
336 lines (273 loc) · 15.2 KB

File metadata and controls

336 lines (273 loc) · 15.2 KB

CFS Notes Worker

Production-grade Cloudflare Worker backend for AI-first note taking with realtime telemetry, semantic search, and usage analytics built on the Cloudflare developer platform.

Cloudflare Workers Durable Objects D1 Analytics Vectorize OpenAI Realtime

Table of Contents

Overview

CFS Notes is a Cloudflare Worker backend that orchestrates authentication, user-scoped Durable Objects, semantic search, and realtime usage telemetry for note-taking clients. Every request passes through src/index.js, which handles CORS, delegates /api/auth/* to Better Auth, issues short-lived HS256 API JWTs, and fans out to Durable Objects, Vectorize, Workers AI, OpenAI Realtime, and D1.

Important

CORS is enforced centrally by corsify(env, req, resp) in src/index.js. Only origins declared in CORS_ORIGIN are mirrored, credentials are always allowed, and custom headers are exposed via the helper.

Highlights

  • Short-lived API credentials/auth/issue mints 3-minute HS256 JWTs with issuer/audience checks before routing to any business API.
  • Realtime session brokering/api/session creates an OpenAI Realtime session (default model gpt-4o-realtime-preview-2025-06-03), stores {internal_session_id → { userId, openai_session_id }} in SessionsDO, and returns a client secret bundle.
  • Notes with semantic searchNotesDO owns CRUD, indexes note IDs, and performs asynchronous vector upserts/deletes in NOTES_VEC using Workers AI embeddings from @cf/baai/bge-small-en-v1.5.
  • Usage telemetry pipeline/api/usage batches sanitized usage events into the ANALYTICS_DB D1 table with strict session validation, while /api/usage/ws proxies WebSocket ingestion through UsageLogDO.
  • Cost-aware summaries/api/usage/summary aggregates per-model token/audio usage and computes estimated spend via src/utils/pricing.js.
  • Operational guardrails — Global OPTIONS handling, structured error handling, retrying vector writes, and centralized response helpers keep the Worker resilient.

Architecture

The Worker builds a hub-and-spoke topology where the router mediates between identity, durable state, vector search, analytics, and third-party AI services.

Runtime Topology

Runtime Topology

Session Flow

Session Flow

Setup

Follow the steps below to bootstrap local development and production-ready deployments.

Pre-flight Checklist

  • Node.js 20+ (Workers runtime parity) and npm 9+
  • Cloudflare account with Workers, D1, Durable Objects, Vectorize, and Workers AI enabled
  • OpenAI API key with Realtime access
  • Wrangler CLI ≥ 4.37 (npm install -g wrangler or use npx)
  • Better Auth CLI (npx @better-auth-cloudflare/cli@latest) if you prefer managed migrations

Tip

Run wrangler login once per machine to authorise the CLI before provisioning databases or deploying.

Clone & Install

git clone <repo-url>
cd cfs-notes
npm install

Provision Cloudflare Resources

Provision services once per account. Replace the names if you prefer different identifiers.

# D1 instances
wrangler d1 create analytics
wrangler d1 create auth-db

# Vectorize index (384 dims for bge-small embeddings)
wrangler vectorize create notes-index --dimensions=384 --metric=cosine

If you are developing remotely, ensure Workers AI is bound to the service (already declared as AI in wrangler.jsonc).

Database Migrations

Apply schema files checked into the repo.

# Better Auth tables
wrangler d1 execute auth-db --file=better-auth-sql/0000_groovy_manta.sql
wrangler d1 execute auth-db --file=better-auth-sql/0001_rapid_warpath.sql

# Usage analytics table (adds status + source columns)
wrangler d1 execute analytics --file=usage-sql/schema.sql

Note

usage-sql/schema.sql is idempotent. Re-running it resets the table because it issues DROP TABLE IF EXISTS. Use with care in production.

Local Development

# Launch the Worker locally but execute Durable Objects / D1 / AI remotely
npx wrangler dev --remote

By default, /auth/issue and downstream APIs expect Better Auth to be reachable at BETTER_AUTH_URL. For a local UI, run Better Auth (or your frontend) on a URL listed in CORS_ORIGIN and proxy requests through the Worker.

Example local vars override for iteration:

// wrangler.jsonc
"vars": {
  "BETTER_AUTH_JWT_SECRET": "local-dev-only-change-me",
  "BETTER_AUTH_URL": "http://localhost:8787",
  "JWT_ISSUER": "cfs-notes",
  "JWT_AUDIENCE": "cfs-notes-frontend",
  "CORS_ORIGIN": "http://localhost:3000",
  "OPENAI_API_KEY": "sk-your-test-key"
}

Warning

Never ship real credentials in wrangler.jsonc. Store sensitive values with wrangler secret put <NAME> before deploying to production.

Deploying

# Cloudflare production deploy
npm run deploy
# ...or directly
wrangler deploy

Confirm BETTER_AUTH_URL, database IDs, and vector index names point at production resources prior to running the deploy. GET /deploy-check returns VERSION_2025_09_26_sessions_do_validation so you can verify the active build.

Configuration

Environment Variables

Variable Required Description Default
BETTER_AUTH_JWT_SECRET HS256 shared secret for issuing and validating API JWTs none
BETTER_AUTH_URL Origin used by Better Auth when delegating /api/auth/* https://cfs-notes-sqlite.username.workers.dev (sample)
JWT_ISSUER Issuer claim enforced in requireUserId() cfs-notes
JWT_AUDIENCE Audience claim enforced in requireUserId() cfs-notes-frontend
CORS_ORIGIN Comma-separated allowlist mirrored by corsify https://notes-cf.vercel.app
OPENAI_API_KEY Token for OpenAI Realtime session API none
DEFAULT_REALTIME_MODEL Optional override for realtime model fallback gpt-4o-realtime-preview-2025-06-03

Set secrets with the Wrangler CLI:

wrangler secret put BETTER_AUTH_JWT_SECRET
wrangler secret put OPENAI_API_KEY

wrangler.jsonc Hints

Update resource identifiers after provisioning.

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "cfs-notes-sqlite",
  "main": "src/index.js",
  "compatibility_date": "2025-03-07",
  "compatibility_flags": ["nodejs_compat"],
  "durable_objects": {
    "bindings": [
      { "name": "NOTES_DO", "class_name": "NotesDO" },
      { "name": "USAGE_DO", "class_name": "UsageLogDO" },
      { "name": "SESSIONS_DO", "class_name": "SessionsDO" }
    ]
  },
  "vectorize": [
    { "binding": "NOTES_VEC", "index_name": "notes-index", "remote": true }
  ],
  "d1_databases": [
    { "binding": "ANALYTICS_DB", "database_name": "analytics", "database_id": "<uuid>" },
    { "binding": "AUTH_DB", "database_name": "auth-db", "database_id": "<uuid>" }
  ]
}

Data Plane

Durable Objects

  • NotesDO (src/durable/notes.js): CRUD plus background vector upserts/deletes. Uses exponential backoff retries when interacting with Vectorize and exposes x-vector-status: pending while embeddings commit.
  • SessionsDO (src/durable/sessions.js): Persists { internal_session_id → { userId, openai_session_id, createdAt } } and serves simple GET/POST endpoints consumed via internal fetches.
  • UsageLogDO (src/durable/usage-log.js): Accepts a proxied WebSocket, ingests JSON usage events, and writes directly to D1 with success/failure acknowledgements.
// src/durable/notes.js (excerpt)
this.ctx.waitUntil(this.upsertVectorInBackground(userId, note));

Vectorize Index

  • Index name: notes-index
  • Dimensions: 384
  • Metric: cosine
  • Metadata stored: { userId, noteId, title }
  • Document IDs: SHA-256 hash of userId:noteId (vecDocId())

D1 Schemas

Analytics storage lives in usage_events. Additional status fields allow reconciliation jobs later on.

CREATE TABLE usage_events (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  user_id TEXT NOT NULL,
  session_id TEXT NOT NULL,
  text_in INTEGER,
  text_out INTEGER,
  audio_in REAL,
  audio_out REAL,
  model TEXT,
  ts TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'provisional',
  source TEXT NOT NULL DEFAULT 'client'
);

Authentication tables are seeded via the Better Auth SQL files in better-auth-sql/ and map directly to the Drizzle schema (src/auth/schema.js).

API Surface

All non-auth routes enforce Authorization: Bearer <api-jwt> via requireUserId(). JWTs expire after 180 seconds; refresh with /auth/issue while the Better Auth session is valid.

Worker Routes

Public health & deploy

  • GET /healthok for monitoring.
  • GET /deploy-check → static version string for post-deploy verification.

Auth delegation & tokens

  • ANY /api/auth/* → forwarded to Better Auth’s auth.handler(req) and automatically CORS-wrapped.
  • POST /auth/issue → returns { token, expiresIn } (HS256 signed with BETTER_AUTH_JWT_SECRET).

Session & identity

  • POST /api/session → creates OpenAI Realtime session, stores DO mapping, responds with:

    {
      "internal_session_id": "uuid",
      "client_secret": {
        "value": "...",
        "expires_at": "2025-04-01T12:34:56Z"
      },
      "model": "gpt-4o-realtime-preview-2025-06-03",
      "openai_session_id": "sess_...",
      "expires_at": "2025-04-01T12:34:56Z"
    }
  • GET /api/me{ userId } extracted from the HS256 payload.

Notes & semantic search

  • GET /api/notes → lists all notes for the authorized user.
  • GET /api/notes?id=<uuid> → fetches a single note.
  • POST /api/notes → creates { title, description }, returns 201 with x-vector-status: pending.
  • PUT /api/notes → updates note by ID, also returns x-vector-status: pending.
  • DELETE /api/notes?id=<uuid> → deletes note and schedules vector removal.
  • POST /api/search → embeds query with Workers AI, queries NOTES_VEC, returns matches [{ id, title, score }].

Usage ingestion & reporting

  • POST /api/usage → accepts { events: [...] } batch, validates session via SessionsDO, inserts into D1.
  • GET /api/usage?date=YYYY-MM-DD → aggregates daily usage by model.
  • GET /api/usage/summary?start=ISO&end=ISO → per-model totals plus cost estimates from pricing.js.
  • WS /api/usage/ws → upgrades to WebSocket, proxied to UsageLogDO for realtime ingestion.
Durable Object Endpoints (internal)
  • SessionsDO:
    • POST https://sessions.internal/put (called from the Worker) → persists mapping.
    • GET https://sessions.internal/get?id=<internal_session_id> → returns mapping or 404.
    • OPTIONS204 for Worker-managed CORS.
  • NotesDO & UsageLogDO are invoked via Durable Object stubs and do not expose public URLs; the Worker wraps them with full CORS-aware responses.

Operational Playbooks

Observability

  • Tail logs: wrangler tail

  • Replay requests: wrangler dev --remote --inspect

  • Inspect Durable Object storage (beta): wrangler dashboard open --service cfs-notes-sqlite

  • Query analytics data:

    wrangler d1 execute analytics --command="SELECT model, SUM(text_in) AS text_in FROM usage_events GROUP BY model;"

Troubleshooting

  • 401 Unauthorized — Verify the HS256 API token is fresh (<3 minutes) and that JWT_ISSUER/JWT_AUDIENCE match between client and Worker.
  • CORS origin rejected — Ensure the frontend origin exactly matches (protocol + host + port) an entry in CORS_ORIGIN.
  • Vector search empty — Check the x-vector-status header on note writes. pending indicates the background Vectorize task has not completed; inspect Workers AI logs if it persists.
  • OpenAI session errors — The Worker throws on non-2xx responses. Inspect console.error output via wrangler tail to capture the OpenAI payload.

Tip

src/durable/notes.js retries vector operations with exponential backoff. If failures persist after five attempts, a warning is logged and the note remains stored for subsequent retries.

Extending Safely

  • Add new API routes in src/index.js after the requireUserId guard to inherit authentication, or before it for public endpoints.
  • Expose extra response headers by passing corsify(env, req, resp, { expose: ["x-new-header"] }) when returning from the router.
  • Expand model pricing in src/utils/pricing.js to keep /api/usage/summary accurate as you onboard new models.
  • Schedule background tasks with ctx.waitUntil(...) inside Durable Objects to keep requests fast.
  • Integrate new AI providers by adding alternate embedText implementations that output the same vector dimensionality.

FAQ

Why do I receive CORS errors after updating the allowlist?

corsify mirrors only exact matches from CORS_ORIGIN. Confirm protocol, host, and port align (e.g., https://app.example.com). Include the localhost port when developing.

How long do issued API tokens last?

POST /auth/issue creates HS256 JWTs valid for 180 seconds. Refresh on the client whenever you receive a 401, or proactively every ~120 seconds.

Can I rotate the HS256 secret without downtime?

Yes. Run wrangler secret put BETTER_AUTH_JWT_SECRET with the new value, deploy, and force clients to fetch fresh tokens. Older tokens will fail verification immediately.

What happens if Workers AI embedding fails?

embedText throws on unexpected responses. The Worker returns 500 and logs the failure. The note remains stored; a subsequent PUT can retry the background upsert.

Resources