Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions api/[...path].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* The single Vercel Function fronting the whole Helpthread engine (HT-43) — a
* catch-all under `/api` that hands every request to the composition root's
* unified handler (`src/composition/root.ts`). Vercel's Node runtime is the
* target (NOT Edge): the engine needs `node:crypto` (HMAC reply tokens,
* AES-GCM token encryption), which the Edge runtime lacks.
*
* ## Why one catch-all + the `fetch` Web Standard export
*
* Vercel's Node runtime supports the `fetch` Web Standard export
* (`export default { fetch(request: Request): Response }`), which handles ALL
* HTTP methods in one function and hands us a web-standard `Request` directly
* — so `createInboxApi`'s framework-agnostic `Request => Response` shape wires
* in with no `node:http` bridge at all (the dev harness's bridge,
* `src/dev/http-adapter.ts`, exists only because a bare `node:http` server
* gives `(req, res)`; Vercel does not). A catch-all `[...path]` file receives
* every `/api/v1/...` path with `request.url` intact, so the engine's own
* router (and the composition root's internal-cron routing) does all path
* dispatch — no per-route function files duplicating that knowledge.
*
* This file is deliberately thin: all wiring lives in the typechecked
* `src/composition/**`. It only awaits the memoized handler and guards against
* a construction/handler failure with a generic 500 (never leaking the error's
* text, which could name a missing env var).
*/

import { getApp } from '../src/composition/root.js'

export default {
async fetch(request: Request): Promise<Response> {
try {
const handler = await getApp()
return await handler(request)
} catch (err) {
// A thrown error here is a build/config failure (getApp rejected) or a
// bug that escaped the handler's own catch-alls. Log server-side; answer
// with the standard, detail-free error envelope.
console.error('[api] failed to build or run the app handler', err)
return new Response(
JSON.stringify({ error: { code: 'server_error', message: 'Internal server error.' } }),
{
status: 500,
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' },
},
)
}
},
}
94 changes: 94 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": false,
"type": "module",
"license": "AGPL-3.0-only",
"description": "Open-source, serverless helpdesk engine \u2014 shared inbox, threaded email, knowledge base \u2014 for teams who live on Vercel and Supabase.",
"description": "Open-source, serverless helpdesk engine shared inbox, threaded email, knowledge base for teams who live on Vercel and Supabase.",
"engines": {
"node": ">=20"
},
Expand All @@ -16,10 +16,12 @@
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"dev:api": "tsx scripts/dev-api.ts"
"dev:api": "tsx scripts/dev-api.ts",
"migrate": "tsx scripts/migrate.ts"
},
"dependencies": {
"@electric-sql/pglite": "^0.5.4",
"@supabase/supabase-js": "^2.110.6",
"jose": "^6.2.3",
"mimetext": "^3.0.28",
"pg": "^8.22.0",
Expand Down
46 changes: 46 additions & 0 deletions scripts/migrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* One-shot database migration runner (HT-43; specs/deploy/gmail-inbound-runbook.md
* Part B2). Applies every migration (`src/db/migrate.ts`) against
* `DATABASE_URL`.
*
* Run ONCE after provisioning the Supabase database, and again whenever new
* migrations are added. The composition root (`src/composition/root.ts`)
* deliberately does NOT migrate on cold start — schema changes are an operator
* step, not something every serverless instance re-runs.
*
* Usage:
* DATABASE_URL='postgres://...' npx tsx scripts/migrate.ts
* # or: npm run migrate (with DATABASE_URL in the environment)
*
* For the one-time DDL you may use the direct (5432) connection string instead
* of the 6543 transaction-mode pooler — either works, since `migrate()`'s
* advisory lock is transaction-scoped and pooler-safe (`src/db/postgres.ts`).
*
* Like `scripts/dev-api.ts`, this lives outside the checked TypeScript project
* (tsconfig `include` covers `src`/`tests`); it is operator tooling run via
* `tsx`, not engine code that ships.
*/

import { migrate } from '../src/db/migrate.js'
import { createPostgresDb } from '../src/db/postgres.js'

async function main(): Promise<void> {
const connectionString = process.env.DATABASE_URL
if (connectionString === undefined || connectionString.trim().length === 0) {
console.error('scripts/migrate: DATABASE_URL is required (the Postgres connection string).')
process.exit(1)
}

const db = await createPostgresDb({ connectionString })
try {
await migrate(db)
console.log('scripts/migrate: all migrations applied.')
} finally {
await db.close()
}
}

main().catch((err: unknown) => {
console.error('scripts/migrate: migration failed', err)
process.exit(1)
})
Loading
Loading