The runtime is a Cloudflare Worker
Your app is built with OpenNext and deployed as a single Worker. There is no Node.js server process. What that rules out:- Work finishes inside the request. There is no process that outlives a
response. Anything periodic belongs in
scheduling-sdk; anything slow belongs behind a job the platform triggers, not asetInterval. - Local files are not storage. Writing to disk is not persistence — use a storage-type Data Store.
- Keep
middleware.tsnamedmiddleware.ts. Next.js 16 deprecates it in favour ofproxy.tsand prints a warning. Do not rename it. Proxy always runs on the Node.js runtime, which the Cloudflare adapter does not support, so the rename produces a build that cannot deploy. The warning is expected.
Bundle size is a hard ceiling
Cloudflare rejects a Worker whose compressed script exceeds 10 MiB. Stardeck warns at 8 MiB, because the failure mode is gradual — every route and every dependency adds to the same bundle.This is the constraint most likely to bite you from a single
npm install. One transitive
dependency — a 3.4 MB WebAssembly blob pulled in by an instrumentation package — pushed the base
template past the limit and had to be stubbed out in next.config.ts. Before adding a heavy
dependency, check whether it is Worker-compatible and what it weighs.npm run build:check in apps/web for a build that does not disturb your dev
server.
Rendered output is not cached between requests
The app ships without an incremental cache configured.open-next.config.ts is
defineCloudflareConfig({}), and with no cache passed, OpenNext resolves
incrementalCache, tagCache and queue to its dummy overrides — every read
and write throws "Dummy" cache does not cache anything and is ignored. So treat
server rendering as per-request: revalidate, revalidateTag and the Next data
cache persist nothing, and background revalidation does not run.
If a page is expensive, cache deliberately — Cache-Control on a route response,
or a cached value in a Data Store — rather than assuming ISR is doing it for you.
Data access
Data lives in Data Stores: Postgres databases owned by your organization, connected to the app by the platform. Your app reads and writes them throughdata-store-sdk, which hands you a Kysely instance backed by
the Neon HTTP driver.
Schema changes are not app code
Creating tables and columns, changing types, and seeding are platform operations, not migrations you commit. They are applied to the same Data Store branch your app reads, and they are versioned by the platform. Do them from the dashboard’s Data Stores editor, or from your editor with Claude Code connected.Files the platform owns
Some files in the repository are generated or maintained by the platform. Edits to them are overwritten on the next upgrade, refused by the editing tools, or both.
Everything else under
apps/web/src is yours.
Changing next.config.ts
The upgrade rail edits this file, so local changes can be reverted by a later
platform version. The common case is adding an image host to
images.remotePatterns.
Ask Stardeck to add it rather than editing the file yourself — the request is
quick and the result survives upgrades. A supported per-app override for
next.config.ts is something we are tracking; until it exists, an edit here is a
change you may have to make twice.
Deploying
apps/web has preview and deploy scripts, but they are the commands the
platform runs — they need Stardeck’s Cloudflare credentials, so running
npm run deploy from your machine fails. Shipping works like this:
- Push to
mainto be seen. The hosted sandbox builds from the platform’smainbranch, not from your working tree. - Publishing to production is a deliberate action in the dashboard. See Publishing and deployment.
- Production and sandbox are different data. See Environments.
Authentication and permissions
Auth isproject-auth, and the visibility boundary is enforced one hop before
your code. The two things that trip people up:
- Declaring a permission key does not grant it. A team member holds an app permission only through what their organization role was granted for that app. Until the grant exists, a correct gate denies everyone.
- API routes are never gated at the edge. A
layout.tsxdoes not wrap route handlers. Gate/api/*inside each handler. access: "internal"on a Surface is authentication, not authorization. It admits every team member. Anything narrower gates itself by permission.
Environment variables
Values come from the platform, not from a committed.env. Pull the sandbox set
with npm run env:pull (see Run your app locally),
and configure them in the dashboard — Environment variables.
- Client exposure is Next.js’s rule: only
NEXT_PUBLIC_-prefixed variables reach the browser. (The dashboard also mirrors non-secret variables with aVITE_prefix for legacy Vite apps; a Next.js app ignores those.) - Secrets are runtime-only. They are not available during the build.
- Do not construct platform URLs by hand. Use
process.env.BASE_URLon the server orprocess.env.NEXT_PUBLIC_BASE_URLin the browser, plus a path. - Never hand-roll a deployment secret. SDK calls sign themselves; if you find
yourself building an
X-Stardeck-Authheader, you are using the wrong entry point.
Cross-surface links are paths, never hostnames
A Surface gets its own hostname in production, but app code must never write that hostname./api/*.
Modules have one public surface
A Module lives atsrc/modules/<name>/ and exposes
index.ts. Deep imports into another Module’s internals — including type imports
and dynamic import() — are a lint error, because install and update swap those
files underneath you.
Modules and Blueprints are in Alpha and available to selected organizations.
Nothing inbound reaches your laptop
Webhooks, scheduled jobs, cross-app calls and integration events are delivered by the platform to a deployed URL. A local dev server has none, and this is a deliberate decision rather than a gap: use the sandbox to exercise inbound traffic. Details in Run your app locally.Operations that do not live in code
These are configured through the platform. Each has a place you do it by hand.If you need one of these and have repository access but no Stardeck account with the right
organization role, you are blocked on a grant, not on code. Ask an organization admin.