11/* DocsPage — public /docs.
22 *
3- * Single-page docs with a sidebar TOC and section anchors. Content lives in
4- * the SECTIONS array below — adding a new section = one entry. No CMS.
3+ * Single-page docs with a sidebar TOC and section anchors. Content lives
4+ * in InstaNode-dev/content/docs/<id>.md, cloned into .content/ at build
5+ * time. Adding/removing a section = one .md file in the content repo;
6+ * no dashboard PR.
57 *
6- * Security note: every code snippet here is reachable by anyone curling the
7- * public domain — they are all anonymous-tier paths. Do not paste production
8- * JWTs, internal cluster hostnames (*.svc.cluster.local), team_ids, or
9- * AES/JWT secrets into snippets. The docs ship to the public domain. */
8+ * Section ordering comes from `order:` frontmatter (numeric).
9+ * Section anchor id = filename minus .md.
10+ *
11+ * Security note: every code snippet here is reachable by anyone curling
12+ * the public domain — they are all anonymous-tier paths. Do not paste
13+ * production JWTs, internal cluster hostnames (*.svc.cluster.local),
14+ * team_ids, or AES/JWT secrets into snippets. The docs ship to the
15+ * public domain. */
1016
1117import { PublicShell } from '../layout/PublicShell'
1218
@@ -16,187 +22,42 @@ type Section = {
1622 body : string // same minimal markdown subset as blog posts
1723}
1824
19- const SECTIONS : Section [ ] = [
20- {
21- id : 'quickstart' ,
22- title : 'Quickstart' ,
23- body : `
24- The whole platform fits in one curl. No signup, no API key, no Docker.
25-
26- \`\`\`
27- curl -X POST https://api.instanode.dev/db/new
28- \`\`\`
29-
30- The response includes a \`connection_url\` you can paste into any Postgres
31- client. The database is real, dedicated, and yours for 24 hours.
32-
33- When you're ready to keep it, see the **Claim flow** section below.
34- `
35- } ,
36- {
37- id : 'services' ,
38- title : 'The six services' ,
39- body : `
40- Every endpoint returns a \`connection_url\` (or \`endpoint\` / \`receive_url\`)
41- plus an \`upgrade_jwt\` you can hand to /claim.
42-
43- - \`POST /db/new\` — Postgres (pgvector pre-installed)
44- - \`POST /cache/new\` — Redis (ACL'd, per-token key prefix)
45- - \`POST /nosql/new\` — MongoDB
46- - \`POST /queue/new\` — NATS JetStream
47- - \`POST /storage/new\` — S3-compatible (MinIO)
48- - \`POST /webhook/new\` — public URL that receives any HTTP method
49-
50- Every response has the same shape: \`{ ok, token, connection_url, internal_url,
51- tier, limits, note, upgrade_jwt }\`. \`internal_url\` is the address to use
52- when the caller itself runs inside our cluster (i.e. via /deploy/new) —
53- public hostnames don't hairpin reliably from inside.
54- `
55- } ,
56- {
57- id : 'deploy' ,
58- title : 'Deploying an app' ,
59- body : `
60- \`POST /deploy/new\` takes a multipart form with a gzipped tar archive
61- containing your Dockerfile + source.
62-
63- \`\`\`
64- curl -X POST https://api.instanode.dev/deploy/new \\
65- -H "Authorization: Bearer <JWT>" \\
66- -F "tarball=@app.tar.gz" \\
67- -F "name=my-app" \\
68- -F "port=8080" \\
69- -F 'env_vars={"DATABASE_URL":"postgres://..."}'
70- \`\`\`
71-
72- The build runs in-cluster on kaniko (~30–90s for typical Node/Python apps)
73- and the app rolls out behind a public HTTPS URL on
74- \`*.deployment.instanode.dev\` with a valid Let's Encrypt cert.
75-
76- \`env_vars\` is optional — pass a JSON object and every key/value lands in
77- the app's environment on the first build. Saves you a follow-up PATCH+redeploy.
78-
79- For multi-service apps see **Stacks** below.
80- `
81- } ,
82- {
83- id : 'stacks' ,
84- title : 'Stacks (multi-service deploy)' ,
85- body : `
86- \`POST /stacks/new\` takes an \`instant.yaml\` manifest plus one tarball per
87- service. Services can reference each other with \`service://<name>\` env
88- values — those resolve to cluster-internal \`http://<name>:<port>\` URLs at
89- deploy time.
90-
91- \`\`\`
92- services:
93- api:
94- build: ./api
95- port: 3000
96- web:
97- build: ./web
98- port: 8080
99- expose: true
100- env:
101- API_URL: service://api
102- \`\`\`
103-
104- Only services with \`expose: true\` get a public URL — the rest are
105- in-cluster only. The whole stack rolls out together; partial failure is
106- reported per-service in \`GET /stacks/{slug}\`.
107- `
108- } ,
109- {
110- id : 'claim' ,
111- title : 'Claim flow (anonymous → paid)' ,
112- body : `
113- Anonymous resources expire in 24 hours. To keep them, claim them.
114-
115- \`\`\`
116- RESP=$(curl -X POST https://api.instanode.dev/db/new -d '{}')
117- JWT=$(echo $RESP | jq -r .upgrade_jwt)
118-
119- # Optional preview — shows what would attach, no side effects
120- curl "https://api.instanode.dev/claim/preview?t=$JWT"
121-
122- # Trigger the claim — sends a magic link to your email
123- curl -X POST https://api.instanode.dev/claim \\
124- -d "{\\"jwt\\":\\"$JWT\\", \\"email\\":\\"you@example.com\\"}"
125- \`\`\`
126-
127- Click the magic link to set a session cookie. Every resource attached to your
128- fingerprint transfers to your team atomically; the connection URLs don't
129- change so any already-running code keeps working.
130-
131- Claimed resources move to your team's tier (hobby by default — $9/mo). There
132- is no separate trial period on paid tiers — **the 24-hour anonymous slice
133- is the trial**.
134- `
135- } ,
136- {
137- id : 'authentication' ,
138- title : 'Authentication' ,
139- body : `
140- Resource provisioning is anonymous. Everything else (deploy, vault, billing,
141- team management) requires a session JWT.
142-
143- How to get one:
144-
145- 1. Provision any resource anonymously. The response includes a JWT in the
146- \`upgrade_jwt\` field.
147- 2. POST that JWT to /claim with an email. We send a magic link.
148- 3. Click the link in the email; the page sets a session cookie.
149-
150- For unattended use (CI, agents), exchange the session cookie for a long-lived
151- API key at \`POST /api/v1/api-keys\`. Pass it as \`Authorization: Bearer
152- <key>\` on every request.
153-
154- To verify a token works at any time, hit \`GET /api/v1/whoami\` — returns
155- 200 with your team_id + plan_tier on success, 401 on failure.
156- `
157- } ,
158- {
159- id : 'limits' ,
160- title : 'Tiers and limits' ,
161- body : `
162- | Tier | Postgres | Redis | MongoDB | TTL | Price |
163- | --------- | ---------- | --------- | ---------- | ---- | ----------- |
164- | Anonymous | 10MB / 2c | 5MB | 5MB / 2c | 24h | free |
165- | Hobby | 1GB / 8c | 50MB | 1GB / 8c | none | $9 / mo |
166- | Pro | 5GB / 20c | 256MB | 5GB / 20c | none | $49 / mo |
167- | Team | coming soon | — | — | — | $199 / mo |
168-
169- "c" = simultaneous connections. The full table is at \`/pricing\`.
170-
171- Limits are enforced at the Postgres user level (\`CONNECTION LIMIT\` on the
172- role) and via per-bucket storage quotas. Exceeding a limit returns a 402 with
173- an upgrade URL — your app keeps running, the next provision just fails.
174- `
175- } ,
176- {
177- id : 'machine-readable' ,
178- title : 'Machine-readable API' ,
179- body : `
180- The full API surface is described in OpenAPI 3.1 at:
181-
182- \`\`\`
183- https://api.instanode.dev/openapi.json
184- \`\`\`
185-
186- It is the source of truth for paths, schemas, security schemes, and which
187- endpoints accept anonymous traffic. Agents reading this spec alone can
188- discover the claim flow (described under \`securitySchemes.bearerAuth\`),
189- the \`/api/v1/whoami\` identity probe, and which fields like \`upgrade_jwt\`
190- to pass forward.
191-
192- If you're an AI agent reading this, the recommended bootstrap is:
25+ const RAW_DOCS = import . meta. glob ( '../../.content/docs/*.md' , {
26+ eager : true ,
27+ query : '?raw' ,
28+ import : 'default' ,
29+ } ) as Record < string , string >
30+
31+ const SECTIONS : Section [ ] = buildSections ( RAW_DOCS )
32+
33+ function buildSections ( raw : Record < string , string > ) : Section [ ] {
34+ return Object . entries ( raw )
35+ . map ( ( [ path , src ] ) => {
36+ const id = path . split ( '/' ) . pop ( ) ! . replace ( / \. m d $ / , '' )
37+ const { meta, body } = parseFrontmatter ( src )
38+ if ( ! meta . title ) return null
39+ return { id, title : meta . title , body : body . trim ( ) , order : Number ( meta . order ) || 0 }
40+ } )
41+ . filter ( ( s ) : s is Section & { order : number } => s !== null )
42+ . sort ( ( a , b ) => a . order - b . order )
43+ . map ( ( { order : _ , ...section } ) => section )
44+ }
19345
194- 1. \`GET /openapi.json\`
195- 2. Provision anonymous resources
196- 3. \`GET /api/v1/whoami\` to confirm token validity once you have one
197- `
46+ function parseFrontmatter ( src : string ) : { meta : Record < string , string > ; body : string } {
47+ const m = src . match ( / ^ - - - \r ? \n ( [ \s \S ] * ?) \r ? \n - - - \r ? \n ? ( [ \s \S ] * ) $ / )
48+ if ( ! m ) return { meta : { } , body : src }
49+ const meta : Record < string , string > = { }
50+ for ( const line of m [ 1 ] . split ( / \r ? \n / ) ) {
51+ const sep = line . indexOf ( ':' )
52+ if ( sep < 0 ) continue
53+ const key = line . slice ( 0 , sep ) . trim ( )
54+ const value = line . slice ( sep + 1 ) . trim ( )
55+ if ( key ) meta [ key ] = value
19856 }
199- ]
57+ return { meta, body : m [ 2 ] }
58+ }
59+
60+ // Unused legacy inline content removed — sections now load from .content/docs/.
20061
20162export function DocsPage ( ) {
20263 return (
0 commit comments