Notion template
A real Notion-style API backend — databases, data sources, pages, blocks, views, comments, file uploads, and search, with Notion's rate limits.
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).
hone env spin notionIt is faithful enough at the URL + wire-format level that an unmodified
@notionhq/client v5 can drive it (see SDK parity).
What's inside
| Piece | Notes |
|---|---|
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
| Mode | Services | Use it for |
|---|---|---|
api (default) | backend | Agents that talk to the REST API or the @notionhq/client SDK. |
This is an API-only template — there is no frontend.
Seeding
hone env spin notion --seed acme-wiki # deterministic fixture on create
hone env seed notion-a1b2 --ai # realistic data via the REST APIAI 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_0123456789abcdef0123456789abcdef0123456789abcdefEvery 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:
- Users —
me, list, retrieve. - Databases — create / retrieve / update / query (with a documented
filter-operator allowlist; unsupported operators return a
400validation 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_sourceobject alias. - File uploads — create / list / retrieve / send / complete.
- Views — create / list / retrieve / update / delete, plus view queries (create / retrieve / delete).
- OAuth token endpoints —
oauth/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.
Inspect a running clone
hone env query notion-a1b2 "select id, title from pages"
hone env db notion-a1b2 # prints a psql shell command
hone env logs notion-a1b2 -f # stream the backendThe 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.