OAuth Action backend for a private or invite‑only Custom GPT to capture and share notes with attribution. Built with Bun + Hono + Zod + zod‑openapi, Clerk OAuth (OIDC), and a single JSON file on a persistent volume.
NEVER use z.record(z.any()) - this is invalid Zod syntax and will cause runtime errors.
ALWAYS use z.record(z.any(), z.any()) - Zod’s record type requires two arguments:
Example:
// ❌ WRONG - Will cause errors
z.record(z.any())
// ✅ CORRECT
z.record(z.any(), z.any()) // any key, any value
z.record(z.string(), z.number()) // string keys, number values
POST /documents — upsert documents with auto-chunkingGET /documents/:id — retrieve specific documentGET /search — full-text search with facets and filteringPOST /annotate — partial updates to documents/chunksPOST /config/ranking — configure search ranking rulesPOST /graphql — GraphQL passthrough to Sourcegraph APIGET /userscripts — list available userscriptsPOST /userscripts/:id/compile — compile userscript with metadataPUT /userscripts/:id/metadata — update metadata with auto-versioning/openapi.json and Swagger UI at /docs for easy Action schema importbun run startcollabGPT/server.agent.md and starts the API on http://localhost:8080.sub=dev_sub, email=dev@example.com.Authorization: Bearer dev:my_sub:me@example.com.CLERK_DEV_ALLOW_INSECURE=1, DEV_SUB, DEV_EMAIL.openid email profile (add more only if required)CLERK_ISSUER=https://<your>.clerk.accounts.devCLERK_AUDIENCE=<your-oauth-client-id-or-aud>The API now includes a complete userscripts management system for browser automation and enhancement.
Dual Router Pattern: We use separate routers for public API endpoints vs internal serving routes:
api/routes/userscripts.ts):
/userscriptsapi/internal/userscripts.ts):
/userscripts (overlapping paths)scripts/userscripts/*.js/userscripts/:id/compile adds metadata headers/userscripts/:id/install in browser
collabgpt_auth)/userscripts/:id/meta for new versions/userscripts/:id/scriptUserscripts use cookie-based auth since they can’t easily manage Bearer tokens:
collabgpt_auth=trueapi/auth.ts before Bearer token validationPOST /documents — upsert document with auto-chunkingGET /documents/:id — retrieve document by IDGET /search — full-text search with filteringPOST /annotate — partial update to document/chunkPOST /config/ranking — update search ranking rulesPOST /graphql — GraphQL passthrough to SourcegraphGET /sourcegraph/status — check configuration statusGET /userscripts — list available userscriptsGET /userscripts/:id — get userscript infoPOST /userscripts/:id/compile — compile with metadataPUT /userscripts/:id/metadata — update metadata (auto-increments version)GET /userscripts/:id/install — HTML installation page (sets cookie)GET /userscripts/:id/script — serve compiled userscriptGET /userscripts/:id/meta — serve metadata block onlyGET /healthz — health checkGET /openapi.json — OpenAPI specificationGET /docs — Swagger UIGET /llms.txt — LLM-friendly docs with OpenAPI YAML/data/kb.json in Fly.io; in dev defaults to ./collabGPT_data/kb.json.rename for atomicity; a .bak snapshot is created on first write.DATA_DIR=/data in production to match your volume mount.bun run collabGPT/server.ts
# PORT=8080 by default
cd collabGPT && bun install && bun run site:build/docs for GitHub Pagescd collabGPT && bun run site:devpaths:
/notes:
get:
operationId: listNotes
summary: List shared notes
responses:
"200": { description: OK }
post:
operationId: createNote
summary: Create a new note attributed to the caller
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [text]
properties:
text: { type: string }
responses:
"200": { description: Created }
/notes/search:
get:
operationId: searchNotes
parameters:
- in: query
name: q
required: true
schema: { type: string }
responses:
"200": { description: OK }
Dockerfile (example):
FROM oven/bun:1.1
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
EXPOSE 8080
ENV PORT=8080
CMD ["bun", "run", "collabGPT/server.ts"]
fly.toml (sketch):
app = "collabgpt-notes"
primary_region = "den"
[build]
dockerfile = "Dockerfile"
[[mounts]]
source = "kb_data"
destination = "/data"
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = true
auto_start_machines = true
Volume:
fly volumes create kb_data --region den --size 1
Env to set in Fly:
CLERK_ISSUERCLERK_AUDIENCEDATA_DIR=/dataPORT=8080Advanced Voice Mode in ChatGPT currently doesn’t trigger external Actions. Include a brief instruction in your GPT for voice users to switch to text when they want to save/read notes.