Skip to content

Contributing

A guide to the code, deployment, technical names and design decisions.

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 repository

Codebase

  • src/

    Browser application and shared source.

    • app/

      The application. Each folder has one role.

      • routes/

        Pages own their data and composition.

      • 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.

  • 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.pageBySlug

Private viewer data

profiles.session

Group 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 reads

Interface

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/shell

The 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

AppHeader

The 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

hero
banner

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.Header

The 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

PageHead

Page toolbar

PageLayout.Toolbar

The 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

PageToolbar

Page content

PageLayout.Content

The 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

card
panel
box

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, tool

An 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.

  1. Check the commit

    CommitGateDeploySkip
    ConvexPREVIOUSGame WorkerPREVIOUSSite WorkerPREVIOUSOpen tabPREVIOUS
    scripts/deploy-decision.ts

    Compare 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.

  2. Deploy compatible Convex functions

    Old browserNew backendoldQuery ✓newQuery ✓
    ConvexNEWGame WorkerPREVIOUSSite WorkerPREVIOUSOpen tabPREVIOUS
    bun run migrations:narrow-check → bun run convex:deploy

    Verify 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.

  3. Finish required migrations

    BackfillLegacy rowsMigrated rows
    ConvexNEWGame WorkerPREVIOUSSite WorkerPREVIOUSOpen tabPREVIOUS
    bun run migrations:deploy

    Run 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.

  4. Build and check the release

    Built + verifiedOld files≥ 7 daysCommitNot live yet
    ConvexNEWGame WorkerPREVIOUSSite WorkerPREVIOUSOpen tabPREVIOUS
    publisher:assets → publisher:retain-assets → publisher:application-runtime:verify

    Build 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.

  5. Deploy the game Worker

    New game WorkerActive commit = release
    ConvexNEWGame WorkerNEWSite WorkerPREVIOUSOpen tabPREVIOUS
    game:dry-run → exact game deploy → game-deployment-contract.ts active

    Deploy 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.

  6. Publish raster variants

    Raster variantsR2, ready first
    ConvexNEWGame WorkerNEWSite WorkerPREVIOUSOpen tabPREVIOUS
    publisher:omit-static-rasters → publish raster variants

    Install 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.

  7. Deploy and check the site Worker

    Open tab, old codeNew visitNewbackend
    ConvexNEWGame WorkerNEWSite WorkerNEWOpen tabPREVIOUS
    exact site deploy → publisher smoke → game binding smoke

    Deploy 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.

  8. Publish Storybook and queue image updates

    StorybookQueue capturesRevision ↑Old image stays until capture succeeds
    ConvexNEWGame WorkerNEWSite WorkerNEWOpen tabPREVIOUS
    Storybook deploy → Renderer revision activation → variant verification

    Publish 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.

Read the production workflowRead how browser files survive a deployment

Release gate

release_gate

The 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

Product glossary

Six kit categories

Content

@ui/content
dataAtreidesOne kind of content

Controls

@ui/control
value + callback, or intentAtreidesSaveonChange(name) / onSave()Change a value or request an action

Lists

@ui/list
items of one shapeAtreidesRepeated items, shared rhythm

Layout

@ui/layout
named slotsLeftRightPositions the slots

Surfaces

@ui/surface
content slotsContent slotPaints the pane

Blocks

@ui/block
data + one slot at mostAtreidesOptional regionA fixed content arrangement

The 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

design system
component library

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

interface
API

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

internal
private
helper

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

wrapper
gateway

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

Shared contract

@shared/*

A schema or rule used by more than one part of the deployed system.

For example, the browser app and Convex use the same faction schema. Shared contracts cannot import the app or game renderers.

Code location: src/shared

Renderer

src/game

A 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.