Architecture
How Sheetbase works: Next.js as OIDC provider, MCP server, and OAuth proxy in one Vercel project.
Overview
Sheetbase is a single Next.js 16 application on Vercel serving three simultaneous roles:
- OAuth 2.1 / OIDC Provider — registers MCP clients with
oauthProvider()and issues resource-bound JWTs withjwt() - MCP Server — handles all 21 tool calls over SSE and HTTP transports
- Google Workspace Proxy — exchanges and auto-refreshes separately granted Google Workspace tokens for Sheets and Drive calls
The product is a private internal beta. There is no separate auth service, API gateway, or sidecar. One repo, one Vercel production slot, and one Supabase backend serve its one private founder-preview runtime.
sequenceDiagram
participant Client as MCP Client
participant Proxy as proxy.ts
participant Core as /api/[transport]
participant DB as Supabase / Postgres
participant Sheets as Google Sheets API
Client->>Proxy: Bearer JWT or x-api-key
Proxy->>Core: Forward request
Core->>DB: Resolve auth + session
Core->>Sheets: Call tool handler
Sheets-->>Core: Result
Core-->>Client: MCP responseRequest flow
MCP Client (Claude / Cursor / API key script)
|
| Bearer JWT -- OIDC path
| x-api-key -- API key path
v
proxy.ts (Next.js request boundary)
|
+-- Enforces MCP Origin/CORS policy
+-- Delegates /.well-known/oauth-* metadata to App Router handlers
|
v
/api/[transport]/route.ts (MCP core)
|
+-- authenticateMcpRequest() -- resolves OAuth JWT or API key → identity
+-- createMcpHandler() -- routes tool name → handler
|
+-- getGoogleClients() -- fetches + proactively refreshes OAuth token
| +-- sheetsClient (Sheets API v4)
| +-- driveClient (Drive API v3)
|
+-- Safety guards -- formula check, schema guard, protected range
|
+-- Google Sheets API v4 → result → MCP tool response → clientKey source files
proxy.ts — OIDC discovery middleware
Applies the MCP Origin/CORS policy and lets the App Router metadata handlers serve the OAuth discovery documents. This allows Claude Desktop, ChatGPT, Cursor, and other compatible clients to discover OAuth without a hardcoded client registration.
Served endpoints:
/.well-known/oauth-authorization-server/.well-known/openid-configuration/.well-known/oauth-protected-resource/.well-known/oauth-protected-resource/{path}(per-resource variant)
/api/[transport]/route.ts — MCP transport
The core of the server. Key facts from the source:
maxDuration: 60— Vercel function timeout for long Sheets API operationsMAX_RESPONSE_ROWS = 1000,MAX_RESPONSE_BYTES = 2MB— response clamp constants[transport]dynamic segment is required —mcp-handleruses it to negotiate SSE vs HTTP. Hardcoding/api/mcpwould break SSE.- Full CORS headers on every response (
Access-Control-Allow-Origin: *)
Keep this file as the transport and registration adapter. New MCP tools belong
in focused modules under src/server/mcp/tools/; do not grow the route with
large business-logic blocks or attempt a big-bang extraction of every tool.
lib/auth.ts — better-auth config
betterAuth({
emailAndPassword: { enabled: false },
socialProviders: { google: {
scope: ['openid', 'email', 'profile'],
prompt: "select_account"
}},
rateLimit: { enabled: true, storage: "database" },
plugins: [
jwt({ jwt: { issuer, audience, expirationTime: "1h" } }),
oauthProvider({
loginPage: "/login",
consentPage: "/authorize",
allowDynamicClientRegistration: true
}),
apiKey({
enableSessionForAPIKeys: true,
rateLimit: { enabled: true, timeWindow: 60_000, maxRequests: 120 }
})
]
})Dashboard sign-in requests identity scopes only. Users grant Sheets and Drive permissions through separate Google Workspace consent from the dashboard.
lib/mcp-google.ts — token retrieval + auto-refresh
Reads the Google OAuth token from the Supabase account table (camelCase columns — always quoted in raw SQL: "accessToken", "userId"). Proactively refreshes if the token expires within 5 minutes. Persists the refreshed token back to Supabase immediately.
// Proactive refresh condition (from source):
const isExpiredOrClose = !expiryDateMs || (expiryDateMs < Date.now() + 5 * 60 * 1000);If refresh fails, continues with the existing token (may still be valid) and clears expiry_date to prevent the google-auth-library from looping on failed auto-refresh.
lib/db.ts — Postgres pool
new Pool({
connectionString: process.env.DATABASE_URL,
max: 3, // max 3 connections — conservative for Vercel serverless
idleTimeoutMillis: 5000,
connectionTimeoutMillis: 5000,
})Two separate Pool instances exist: one in lib/db.ts (used by MCP tools) and one inside lib/auth.ts (used by better-auth). This is a known technical debt item — they are not consolidated.
Postgres query shape matters for spreadsheet writes. transform_range uses
rowMode: 'array' so repeated unnamed SQL columns are preserved instead of
being collapsed into duplicate object keys. analyze_range keeps object rows
for readable analysis output.
src/instrumentation.ts — OpenTelemetry
// ConsoleSpanExporter → stdout
// Designed specifically for agent-visible trace output:
// "Gives the Antigravity Agent native X-Ray vision to read traces
// directly via 'command_status' output logs"Traces are exported to console stdout only — not to a remote OTLP collector. This is intentional for the current build stage: traces are visible in Vercel function logs.
Database schema
Tables (from migrations + SQL scripts)
| Table | Purpose |
|---|---|
user | better-auth user accounts |
session | active sessions |
account | OAuth tokens per provider (Google) — camelCase columns |
verification | email verification tokens |
oauthClient | OAuth client registrations (Claude, ChatGPT, Cursor) |
oauthRefreshToken | OAuth refresh tokens issued to MCP clients |
oauthAccessToken | OAuth access tokens issued to MCP clients |
oauthConsent | User consent records for registered MCP clients |
rateLimit | Better Auth database-backed request limits |
apikey | API keys |
sheet_snapshots | Range data cache + per-sheet __schema__:<sheet> cache (TTL-based) |
write_history | Pre-write snapshots for restore_snapshot (max 50 per user+spreadsheet) |
sheet_snapshots
Dual-purpose cache table:
- Range data cache (key = normalized A1 range, TTL = 60s) — used by
analyze_range - Schema cache (key =
__schema__:<sheet>, TTL = 300s) — used by all write tools for formula/protected-range guards
Range caches are invalidated on every successful write. Structural writes clear all cache entries for the spreadsheet.
write_history + prune_write_history()
Stores before_values JSONB snapshots before every write_range and transform_range call. A Postgres function prune_write_history() is called after every insert to keep the last 50 snapshots per user+spreadsheet. Not a trigger — called explicitly.
sheet_analyzer Postgres role
A restricted role with no table privileges, used by analyze_range and transform_range via SET LOCAL ROLE sheet_analyzer. Cannot read account, user, session, apikey, sheet_snapshots, or write_history — even if SQL injection bypasses the regex guard.
Deployment
The hosted Sheetbase product is a private internal beta with one private
founder-preview runtime at https://sheetbase.flonest.app. Feature PRs target
main. An approved merge to main automatically releases the exact merged tree
through .github/workflows/release-internal-beta.yml, the only publisher.
If the merged commit is not attributed to the protected repository ADMIN, the
first run adds one ADMIN-attributed empty commit with the same tree and stops.
The second workflow run performs the complete release for that unchanged tree.
The workflow verifies, safely handles eligible migrations, stages, smoke-tests,
promotes, and can restore the previous deployment. Native Git deployment and a
separate beta lane are not used.
Required environment variables
| Variable | Source |
|---|---|
DATABASE_URL | Supabase Postgres connection string |
BETTER_AUTH_URL | Full app URL (e.g. https://sheetbase.flonest.app) |
NEXT_PUBLIC_APP_URL | Same as above |
GOOGLE_CLIENT_ID | Google Cloud Console → OAuth 2.0 Client ID |
GOOGLE_CLIENT_SECRET | Google Cloud Console → OAuth 2.0 Client Secret |
BETTER_AUTH_SECRET | HMAC signing key for sessions |
NEXT_PUBLIC_SUPABASE_URL | Supabase project URL (required even if SSR not used) |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Supabase anon key |
Warning: NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY must be set. The @supabase/ssr package is in the dependency tree and the Edge runtime will throw 500 if these are missing, even if you don't use Supabase SSR middleware.
Google Cloud Console requirements
- Sheets API enabled
- Drive API enabled
- OAuth consent screen configured (add test users if in "Testing" mode)
- OAuth Client ID (Web Application) with redirect URI:
https://sheetbase.flonest.app/api/auth/callback/google