Production-grade Cloudflare Worker backend for AI-first note taking with realtime telemetry, semantic search, and usage analytics built on the Cloudflare developer platform.
- Overview
- Highlights
- Architecture
- Setup
- Configuration
- Data Plane
- API Surface
- Operational Playbooks
- Extending Safely
- FAQ
- Resources
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.
- Short-lived API credentials —
/auth/issuemints 3-minute HS256 JWTs with issuer/audience checks before routing to any business API. - Realtime session brokering —
/api/sessioncreates an OpenAI Realtime session (default modelgpt-4o-realtime-preview-2025-06-03), stores{internal_session_id → { userId, openai_session_id }}inSessionsDO, and returns a client secret bundle. - Notes with semantic search —
NotesDOowns CRUD, indexes note IDs, and performs asynchronous vector upserts/deletes inNOTES_VECusing Workers AI embeddings from@cf/baai/bge-small-en-v1.5. - Usage telemetry pipeline —
/api/usagebatches sanitized usage events into theANALYTICS_DBD1 table with strict session validation, while/api/usage/wsproxies WebSocket ingestion throughUsageLogDO. - Cost-aware summaries —
/api/usage/summaryaggregates per-model token/audio usage and computes estimated spend viasrc/utils/pricing.js. - Operational guardrails — Global OPTIONS handling, structured error handling, retrying vector writes, and centralized response helpers keep the Worker resilient.
The Worker builds a hub-and-spoke topology where the router mediates between identity, durable state, vector search, analytics, and third-party AI services.
Follow the steps below to bootstrap local development and production-ready deployments.
- 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 wrangleror 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.
git clone <repo-url>
cd cfs-notes
npm installProvision 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=cosineIf you are developing remotely, ensure Workers AI is bound to the service (already declared as AI in wrangler.jsonc).
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.sqlNote
usage-sql/schema.sql is idempotent. Re-running it resets the table because it issues DROP TABLE IF EXISTS. Use with care in production.
# Launch the Worker locally but execute Durable Objects / D1 / AI remotely
npx wrangler dev --remoteBy 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:
Warning
Never ship real credentials in wrangler.jsonc. Store sensitive values with wrangler secret put <NAME> before deploying to production.
# Cloudflare production deploy
npm run deploy
# ...or directly
wrangler deployConfirm 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.
| 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_KEYUpdate 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>" }
]
}NotesDO(src/durable/notes.js): CRUD plus background vector upserts/deletes. Uses exponential backoff retries when interacting with Vectorize and exposesx-vector-status: pendingwhile embeddings commit.SessionsDO(src/durable/sessions.js): Persists{ internal_session_id → { userId, openai_session_id, createdAt } }and serves simpleGET/POSTendpoints 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));- Index name:
notes-index - Dimensions:
384 - Metric:
cosine - Metadata stored:
{ userId, noteId, title } - Document IDs: SHA-256 hash of
userId:noteId(vecDocId())
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).
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
GET /health→okfor monitoring.GET /deploy-check→ static version string for post-deploy verification.
ANY /api/auth/*→ forwarded to Better Auth’sauth.handler(req)and automatically CORS-wrapped.POST /auth/issue→ returns{ token, expiresIn }(HS256 signed withBETTER_AUTH_JWT_SECRET).
-
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.
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 }, returns201withx-vector-status: pending.PUT /api/notes→ updates note by ID, also returnsx-vector-status: pending.DELETE /api/notes?id=<uuid>→ deletes note and schedules vector removal.POST /api/search→ embedsquerywith Workers AI, queriesNOTES_VEC, returns matches[{ id, title, score }].
POST /api/usage→ accepts{ events: [...] }batch, validates session viaSessionsDO, 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 frompricing.js.WS /api/usage/ws→ upgrades to WebSocket, proxied toUsageLogDOfor 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.OPTIONS→204for Worker-managed CORS.
NotesDO&UsageLogDOare invoked via Durable Object stubs and do not expose public URLs; the Worker wraps them with full CORS-aware responses.
-
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;"
- 401 Unauthorized — Verify the HS256 API token is fresh (<3 minutes) and that
JWT_ISSUER/JWT_AUDIENCEmatch 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-statusheader on note writes.pendingindicates 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.erroroutput viawrangler tailto 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.
- Add new API routes in
src/index.jsafter therequireUserIdguard 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.jsto keep/api/usage/summaryaccurate 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
embedTextimplementations that output the same vector dimensionality.
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.
POST /auth/issue creates HS256 JWTs valid for 180 seconds. Refresh on the client whenever you receive a 401, or proactively every ~120 seconds.
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.
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.