Start here
Dune Zone is open source. You can work on the application, improve its artwork or help explain how its parts fit together.
Start with local setup, then use the repository map to find the code you need. The glossary explains the names you will see in that code.
The examples run in your browser. You can change their inputs without saving records or deploying code.
Open the source repositoryCodebase
- src/
Browser application and shared source.
- app/
The application. Each folder has one role.
- routes/
Pages own their data and composition.
- ui/
Kit components render caller-supplied data.
- widgets/
Assemblies shared by multiple pages.
- pickers/
Lazy, read-only option loading.
- shell/
The persistent header, navigation and footer.
- db/
The application's doorway to Convex.
- print/
Records turned into sheets and publisher captures.
- shared/
Schemas and rules used by the browser, Convex and Workers.
- game/
Game renderers for screen and print, separate from the app.
- convex/
Database schema, server functions and migrations.
- workers/
Separately deployed Cloudflare Workers.
- docs/
Architecture and contributor workflows.
- .github/workflows/
Checks and deployment automation.
- package.json
Scripts and the dependency catalogue.
- AGENTS.md
Instructions and vocabulary for work in this repository.
Data
Most public pages read two kinds of data. A page query loads public content, such as a faction. A separate private query loads the viewer's profile or session.
Public page data
factions.pageBySlugPrivate viewer data
profiles.sessionGroup access can add a small groups.viewerMembership query. Mutations check permission again on the server.
Why the split improves query caching
Convex can reuse a public query result for visitors who request the same arguments. If that query also reads the viewer's identity, the result can vary by visitor. We keep profile and session reads separate so public content can share a cached result.
Shared page query
1 public page result for 3 viewers
Our publicQuery builder prevents access to ctx.auth. A separate private query returns the viewer's profile or session.
Viewer-dependent page query
Up to 3 page-result variants
Reading identity in the page query can produce a different cached result for each viewer.
This models distinct results for one query and one slug. It does not measure network requests, execution time or cache hit rate.
Public page and private viewer hooks
const page = useFaction(slug, { initialData });
const viewer = useCurrentProfile();
// Share page content. Read the viewer separately.
// Access-aware screens also use useFactionAccess(pageData).Separate page data from viewer data · example diff
-return { faction, viewerProfile, canEditForThisViewer };+return { faction, owner, assignedGroup };+// Combine page data with the viewer query in the browser.These are two kinds of data, not a fixed subscription count. A page may need an additional access query.
Read the public and private query rulesSee the private session and Group membership readsInterface
The header carries the title and introduction. The optional toolbar carries page-wide controls. Content is the main landmark. AppFooter belongs to the shell, so PageLayout has no footer slot.
An application page · TSX
<PageLayout>
<PageLayout.Header>
<PageTitle title="Example" />
</PageLayout.Header>
<PageLayout.Toolbar>
<Toolbar>
<Toolbar.Center>Page-wide controls</Toolbar.Center>
</Toolbar>
</PageLayout.Toolbar>
<PageLayout.Content>
<Section title="Details">…</Section>
</PageLayout.Content>
</PageLayout>Use the named slot · example diff
-<header><h1>Example</h1></header>+<PageLayout.Header>+ <PageTitle title="Example" />+</PageLayout.Header>Chrome
src/app/shellThe frame around ordinary application pages.
It includes the artwork band, site navigation and footer. Chrome is named for its position around the page, so it is separate from the six kit categories.
Code location: src/app/shell
Band
AppHeaderThe artwork across the top of the application.
The page header appears over it. On an immersive page, the band fills the window.
Code location: src/app/shell/AppHeader.tsx
Names to avoid
PageLayout
The frame each application page renders.
It has Header, Toolbar and Content slots. PageLayout responds to the window width; other layouts respond to their container width.
Code location: src/app/ui/layout/PageLayout.tsx
Page header
PageLayout.HeaderThe slot for the page title and introduction, over the artwork band.
Pages that omit it use the compact layout.
Code location: src/app/ui/layout/PageLayout.tsx
Names to avoid
Page toolbar
PageLayout.ToolbarThe slot between Header and Content.
It holds a Toolbar for page-wide search, filters and actions.
Code location: src/app/ui/layout/PageLayout.tsx
Names to avoid
Page content
PageLayout.ContentThe slot for the page's main content.
It provides the main landmark used by screen readers.
Code location: src/app/ui/layout/PageLayout.tsx
PageTitle
The page's top-level heading, with an optional short label above it.
The route renders one PageTitle inside PageLayout.Header.
Code location: src/app/ui/block/PageTitle.tsx
Toolbar
A pane with actions at the edges and search or filters in the centre.
It has one primary action. At narrow widths, status labels collapse to icons instead of wrapping.
Code location: src/app/ui/surface/Toolbar.tsx
Surface
A component that draws a pane with a background and border.
Other kit components remain transparent. A Surface cannot contain another Surface.
Code location: src/app/ui/surface/Surface.tsx
Names to avoid
Section
A region of content with a heading.
Its nesting depth determines the heading level and size.
Code location: src/app/ui/block/Section.tsx
Slot
<TriptychLayout.Left>A named region that a layout positions, written as a child component such as PageLayout.Content.
Every layout has at least two slots.
Code location: src/app/ui/layout
Adornment
icon, action, toolAn icon, action button or hover text attached to a component.
These decorate the component and do not count towards its slot limit.
Code location: AGENTS.md
How it ships
Convex, the game Worker and the site Worker deploy separately. During a release, the previous browser bundle runs against the new backend. The deployment order has to support that combination.
Check the commit
scripts/deploy-decision.tsCompare the requested commit with both live Worker identities. Skip the release if it is already deployed or a newer commit has replaced it.
This gate decides whether to deploy the commit. Pull request checks run separately.
Deploy compatible Convex functions
bun run migrations:narrow-check → bun run convex:deployVerify that any schema narrowing is allowed, then deploy functions and schema before publishing the application.
The site still serves the previous browser bundle. Every public query that bundle calls must remain available.
Finish required migrations
bun run migrations:deployRun and verify the migrations the release requires, then initialize missing publication settings.
Existing clients can still write while this happens. Migration code and the schema must continue accepting those writes.
Build and check the release
publisher:assets → publisher:retain-assets → publisher:application-runtime:verifyBuild browser and server artifacts from the same commit, retain the previous release's hashed browser files, and exercise the assembled public application locally.
Old hashed browser files remain available for at least seven days after replacement. Runtime checks must pass before the site upload.
Deploy the game Worker
game:dry-run → exact game deploy → game-deployment-contract.ts activeDeploy the private game service and require its active identity before the site Worker connects to it.
An upload alone does not prove the game Worker is serving the new version. The check reads its active release identity.
Publish raster variants
publisher:omit-static-rasters → publish raster variantsInstall the publish token on the live Worker and upload the generated image variants before the new site starts referencing them.
A new browser must not request artwork that has not reached storage yet.
Deploy and check the site Worker
exact site deploy → publisher smoke → game binding smokeDeploy the verified artifact, check its release identity, and test the game through the connection used by dune.zone.
New requests receive the new site bundle. Already-open tabs may still run the previous bundle, so the backend must still support those clients.
Publish Storybook and queue image updates
Storybook deploy → Renderer revision activation → variant verificationPublish the isolated Storybook, activate any increased renderer revisions and verify the release's media. A separate job then rebuilds the shared development environment.
Increasing a renderer revision queues new captures. Pages can show the previous published image until its replacement is ready.
Why old browsers still matter
After Convex deploys, the site still serves the previous browser bundle. Open tabs can keep running it even after the site deploys. If the backend removes a query that bundle calls, the page can crash during render.
Replace a query across separate releases · example diff
-// One release: delete the old query and update the browser.+// Release A: keep oldQuery, add newQuery, move the browser.+// Later release: remove oldQuery once clients no longer need it.If a later stage fails
A failed stage stops the workflow, but does not undo the artifacts already deployed. Inspect the live identities, correct the failing input and deploy the fix through the workflow. Keep the compatibility and asset-retention gates in place.
Release gate
release_gateThe first deployment job.
It checks the live Worker commits and skips releases that are already deployed or have been superseded.
Code location: .github/workflows/deploy-main.yml
Where it runs
- Site and publisher WorkerServes dune.zone and renders public pages on the server. Its scheduled publisher processes image jobs every five minutes.
- Game WorkerRuns a room for each hosted game and stores its live table state.
- ConvexStores application records and publication jobs, and handles authentication.
- R2 storageKeeps one current image per published asset.
- Storybook WorkerServes storybook.dune.zone.
Technical glossary
Six kit categories
Content
@ui/contentControls
@ui/controlLists
@ui/listLayout
@ui/layoutSurfaces
@ui/surfaceBlocks
@ui/blockThe input decides the category. Layouts and Surfaces receive built content through slots. Blocks, Lists and Content turn data into content. Controls report changes or action intents.
Kit
@ui/*The components under src/app/ui, grouped into Content, Controls, Lists, Layout, Surfaces and Blocks.
They render caller-supplied data and report user actions through callbacks. They may use record types, but cannot fetch data or navigate. The kit belongs to this application.
Code location: src/app/ui
Names to avoid
Membrane
The contract between a component and its caller.
Data, values and slots enter through props. Chosen values and user intents leave through callbacks. The caller handles effects such as fetching, saving and navigation.
Code location: AGENTS.md
Names to avoid
Organ
A file used only by its own feature or kit machinery.
Nothing outside that feature may import it, and it has no story. An outside importer or a story means it needs a different classification.
Code location: AGENTS.md
Names to avoid
Doorway
The only allowed import path to otherwise restricted code.
src/app/db is the doorway to Convex. ApplicationChrome and AppNotFound are the doorway to the shell.
Code location: src/app/db
Names to avoid
Seam
A place where one implementation can replace another, including in tests.
The live-query module is the seam between the app and Convex.
Code location: src/app/db/core/live.ts
Widget
An assembly of components shared by at least two pages, such as the faction editor.
A large widget may also split into child widgets. Widgets receive data and callbacks from their caller; they cannot fetch or navigate.
Code location: src/app/widgets
Picker
A control that loads its own options when opened, or when an open popover mounts it.
It reads through src/app/db, releases the subscription when the options close and reports a choice through a callback. Pickers live outside the kit and are the only components allowed to fetch.
Code location: src/app/pickers
Renderer
src/gameA component that draws a card, token or other game asset for screen and print.
Renderers cannot import the app or Mantine. The app and publisher browser use the same rendering code.
Code location: src/game
Design decisions
Each decision gives a rule, explains why we use it and links to its source in the repository.
The badge tells you how we enforce the rule. Some have a lint rule, test or check script. Others show a warning during development or need a person to check them in review.
Where an automated check covers only part of a rule, the entry says what still needs review.
