Skip to main content
Sheetbase Docs
Concepts

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:

  1. OAuth 2.1 / OIDC Provider — registers MCP clients with oauthProvider() and issues resource-bound JWTs with jwt()
  2. MCP Server — handles all 21 tool calls over SSE and HTTP transports
  3. 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 response

Request 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 → client

Key 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 operations
  • MAX_RESPONSE_ROWS = 1000, MAX_RESPONSE_BYTES = 2MB — response clamp constants
  • [transport] dynamic segment is requiredmcp-handler uses it to negotiate SSE vs HTTP. Hardcoding /api/mcp would 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)

TablePurpose
userbetter-auth user accounts
sessionactive sessions
accountOAuth tokens per provider (Google) — camelCase columns
verificationemail verification tokens
oauthClientOAuth client registrations (Claude, ChatGPT, Cursor)
oauthRefreshTokenOAuth refresh tokens issued to MCP clients
oauthAccessTokenOAuth access tokens issued to MCP clients
oauthConsentUser consent records for registered MCP clients
rateLimitBetter Auth database-backed request limits
apikeyAPI keys
sheet_snapshotsRange data cache + per-sheet __schema__:<sheet> cache (TTL-based)
write_historyPre-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

VariableSource
DATABASE_URLSupabase Postgres connection string
BETTER_AUTH_URLFull app URL (e.g. https://sheetbase.flonest.app)
NEXT_PUBLIC_APP_URLSame as above
GOOGLE_CLIENT_IDGoogle Cloud Console → OAuth 2.0 Client ID
GOOGLE_CLIENT_SECRETGoogle Cloud Console → OAuth 2.0 Client Secret
BETTER_AUTH_SECRETHMAC signing key for sessions
NEXT_PUBLIC_SUPABASE_URLSupabase project URL (required even if SSR not used)
NEXT_PUBLIC_SUPABASE_ANON_KEYSupabase 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

See Also

On this page