@matters/static (apps/static, dev port 4005) hosts per-user static pages at
static.matters.ai on top of Vercel Blob (@vercel/blob). Each user owns slugs; each slug renders
uploaded files. There is no shared database — Blob JSON documents are the store.
proxy.ts)#Clerk middleware with a prefix allowlist. Public prefixes: the landing page, /v + /v1 renderers,
sign-in/sign-up, /dashboard, /complete-profile, /forbidden, /access-denied, plus internal
(api, _next, favicon.ico, robots.txt, sitemap.xml). Everything else — notably
/[username]/* — requires auth.protect().
/[username]/*#/dashboard — signed-in redirect: no identity → /sign-in; no username → /complete-profile;
otherwise → /<username>./[username] — the owner's page list; /[username]/editor — upload/manage;
/[username]/settings and /[username]/agreements — page settings.lib/auth.ts (getOwnerIdentity)./v/[slug]#/v/[slug]/route.ts (+ /v/[slug]/[...path]/route.ts for sub-paths) serves the entry file and
assets for a slug. /v1 is the legacy renderer route.
lib/static-store.ts)#users/<username>/pages/<pageId>/meta.json — StaticPageRecord
slugs/<slug>.json — SlugRecord (slug → username + pageId)
A StaticPageRecord holds pageId, username, slug, title, passwordHash (nullable —
lib/password.ts gates protected pages), files[] (path, blobUrl, contentType, size),
entryFile, and timestamps. Slugs are opaque 16-char ids (createOpaqueSlug). Reads use
cache: "no-store" fetches; writes put with addRandomSuffix: false.
| Route | Purpose |
|---|---|
GET /api/health | Health check. |
GET /api/profile/username | Username/profile lookup. |
GET/POST /api/pages | List / create pages. |
GET/PATCH/DELETE /api/pages/[pageId] | Page CRUD. |
POST /api/upload | File upload into a page. |
@matters/static sits among the apps