Skip to main content
The Notion template (apps/notion-clone, name notion) is an API-only clone of the Notion public API: a NestJS backend on Postgres that speaks Notion’s REST surface — integration-token Bearer auth with a Notion-Version header, the workspace → users → databases → data sources → pages → blocks → comments object model, Notion’s rich property/block JSON, cursor pagination returning the {object:'list', results, has_more, next_cursor, type} envelope, and Notion’s per-token rate limit (429 rate_limited + Retry-After).
asymmetric spin notion
It is faithful enough at the URL + wire-format level that an unmodified @notionhq/client v5 can drive it (see SDK parity).

What’s inside

PieceNotes
backend/NestJS REST API, integration-token Bearer auth, per-token rate limiter, Postgres via pg. Health at /api/health; API at /api/v1/....
supabase/migrations/001_initial_schema.sql (workspaces, users, integration_tokens, databases, pages, blocks, comments) and 002_views_and_file_uploads.sql (file_uploads, views, view_queries) — run on spin and replayed on reset.
seeds/acme-wiki.sql — the bundled deterministic fixture (an “Acme Wiki” workspace, a bot user + person users, a “Tasks” database, pages, blocks, comments, and a seeded integration token).

Modes

ModeServicesUse it for
api (default)backendAgents that talk to the REST API or the @notionhq/client SDK.
This is an API-only template — there is no frontend.

Seeding

asymmetric spin notion --seed acme-wiki     # deterministic fixture on create
asymmetric seed notion-a1b2 --ai            # realistic data via the REST API
AI seeding generates users, databases, and pages (the template’s declared entities). See Seeding.

The seeded integration token

Unlike a browser OAuth install flow, the clone seeds an integration token so the first curl or SDK call authenticates with zero setup (there is no signup/consent route):
ntn_0123456789abcdef0123456789abcdef0123456789abcdef
Every request needs both the Bearer token and a Notion-Version header:
curl -s http://127.0.0.1:3004/api/v1/users/me \
  -H "Authorization: Bearer ntn_0123456789abcdef0123456789abcdef0123456789abcdef" \
  -H "Notion-Version: 2026-03-11"
A missing Notion-Version header returns the real Notion error (400 missing_version); a present header is accepted regardless of value (known versions 2022-06-28 / 2025-09-03 / 2026-03-11 are recognized, unknown ones are normalized rather than rejected) so modern SDKs keep working. Invalid or missing Bearer tokens return 401 {code:'unauthorized'}.

Rate limiting

Like real Notion, the clone enforces a per-integration-token rate limit (token bucket, ~3 requests/second). Bursting past it returns Notion’s throttle response byte-for-byte:
HTTP 429
Retry-After: 1
{"object":"error","status":429,"code":"rate_limited","message":"..."}
A separate 529 service_overload path also exists (env-gated). The health probe is exempt. Agents that retry on Retry-After — as the official SDK does — work unchanged.

SDK parity

An unmodified @notionhq/client v5+ (which sends Notion-Version: 2026-03-11) works against the clone when constructed with the clone’s baseUrl:
import { Client } from '@notionhq/client';

const notion = new Client({
  auth: 'ntn_0123456789abcdef0123456789abcdef0123456789abcdef',
  baseUrl: 'http://localhost:3004/api', // SDK appends /v1/... → /api/v1/...
});

await notion.users.me();                              // bot handshake
const db = await notion.databases.retrieve({ database_id });
const ds = db.data_sources[0].id;                     // 2025-09-03 data_source
await notion.dataSources.query({ data_source_id: ds });
await notion.pages.create({ parent: { data_source_id: ds }, properties });
The clone ships a thin data_sources facade: each database exposes exactly one data source whose id equals the database id, so the v5 SDK’s data_sources-routed calls (dataSources.query, parent.data_source_id, search({filter:{property:'object',value:'data_source'}})) resolve.

API shape — read this before pointing an agent at it

The clone is a path-faithful subset of Notion’s REST API under the /api/v1 prefix (POST /api/v1/pages, POST /api/v1/databases/:id/query, …). The template ships an API_PARITY.md that scores it against Notion’s live docs (re-fetched every audit, never from memory). The implemented core covers:
  • Usersme, list, retrieve.
  • Databases — create / retrieve / update / query (with a documented filter-operator allowlist; unsupported operators return a 400 validation error rather than silently returning all rows).
  • Data sources (2025-09-03 split) — create / retrieve / update / query.
  • Pages — create / retrieve / update (incl. trash via in_trash) / property item / move / Markdown read + write.
  • Blocks — retrieve / list children / append children / update / delete.
  • Comments — create / list / retrieve / update / delete.
  • Search — pages + databases by title, with the data_source object alias.
  • File uploads — create / list / retrieve / send / complete.
  • Views — create / list / retrieve / update / delete, plus view queries (create / retrieve / delete).
  • OAuth token endpointsoauth/token (incl. grant_type=refresh_token) / oauth/introspect / oauth/revoke.
That’s 45 endpoints at 100% shape parity against the current live surface. Surfaces outside the targeted core — webhooks / Events API, the hosted MCP server (mcp.notion.com), and relation/rollup/formula compute — are unimplemented by design; the clone never claims to emit events or ship MCP tools. See apps/notion-clone/API_PARITY.md for the per-endpoint breakdown and the filter-operator allowlist.
The verify command will fold live fidelity scoring into the CLI. Until then, API_PARITY.md and the /verify-api workflow are how fidelity is tracked.

Inspect a running clone

asymmetric query notion-a1b2 "select id, title from pages"
asymmetric db notion-a1b2          # prints a psql shell command
asymmetric logs notion-a1b2 -f     # stream the backend
The schema (workspaces, users, databases, pages, blocks, comments, plus file_uploads / views / view_queries) comes straight from the two migration files — read them to know what’s queryable.