Molta documentation
Everything you need to integrate, deploy, or extend Molta — written to be implementable by a developer or an AI. For AI ingestion, the same content is available as a single plaintext file at /docs/llms.txt.
Overview & architecture
Molta is a collaboration system for game/app developers working with remote contractors (artists, sound designers). Developers define the assets a game needs, brief them with requirements + placeholders, invite contractors to upload new versions, review and finalize them, and stream the finished assets into the app at runtime via a 6-digit access code — or bake them into a production build.
Components
| Component | What it is |
|---|---|
| Web portal | Next.js 14 (App Router) app on Vercel. Auth0 login. The collaboration UI + all APIs. |
| Database & storage | Supabase Postgres (metadata/workflow) + Supabase Storage (asset files, private bucket, signed URLs). |
| Runtime SDK | MoltaKit — a Swift package the game embeds to pull finalized assets. Other platforms speak the same HTTP contract. |
| CLI | molta — a zero-dependency Node CLI to seed assets, bake production builds, and manage schema versions. |
| Companion app | A SwiftUI iOS app for collaborating on the go (conversations, push, photo upload, review). |
How the pieces talk
Developer ─┐ Auth0 login Contractor ─┐ Auth0 login
▼ ▼
┌──────────────────────────────────────────────────┐
│ Web portal (Next.js / Vercel) │
│ Auth0 · Supabase Postgres · Supabase Storage │
└───┬───────────────┬───────────────────┬───────────┘
6-digit code Bearer apt_ token Auth0 Bearer JWT
│ │ │
▼ ▼ ▼
MoltaKit molta CLI iOS companion app
(game runtime) (seed / bake) (collaborate on phone)
Three distinct auth paths
Molta deliberately uses different credentials for different clients:
- Developers & contractors use Auth0 (web) — and the iOS companion app uses Auth0 native login (Bearer access token).
- The game's runtime SDK uses the portal's 6-digit access code (a read key) — end users never log in.
- The CLI / automation uses a hashed API token (
apt_…, machine-to-machine).
Repository layout
apps/portal Next.js web app + all APIs (deploy this to Vercel)
supabase/schema.sql Postgres schema (source of truth for the data model)
swift/MoltaKit Runtime Swift SDK the game embeds
apps/ios SwiftUI companion app (XcodeGen project)
packages/cli molta CLI
examples/sample-game Galaxy Raiders — a runnable demo + assets
Core concepts
Portal (project)
One portal per game/app. It owns sections, groups, assets, members, an immutable 6-digit access code, an asset schema version, and conversations.
Asset
A single deliverable the game needs — an image, video, audio clip, music track, sound effect, level, model, font, text, or data file. Each asset has:
- a stable
asset_key(lower_snake_case, e.g.hero_idle) — the id the SDK uses; never rename it. - a type, a brief (free-text description), and structured requirements (dimensions, duration, loop, format, size budget…).
- a lifecycle status and an ordered list of versions (uploads).
Section & group
- Sections are loose organizational buckets ("Character Art", "Soundtrack").
- Groups bundle assets that must ship together (e.g. animation frames). The contractor must upload every asset in a group before they can Share it, and the whole group is reviewed as a unit. An ungrouped asset behaves like a group of one.
Asset lifecycle
placeholder → in_progress → submitted → in_app → done
└──────────────┴──→ rejected
| Status | Meaning |
|---|---|
placeholder | Developer seeded a stand-in; awaiting the contractor's first upload. |
in_progress | Contractor has uploaded a draft (not yet shared) or changes were requested. |
submitted | Contractor shared the work; waiting on developer review. |
in_app | Developer published the version live for preview ("See in app"). |
done | Developer signed off; the contractor is released. |
rejected | Developer rejected the current version. |
Upload ≠ submit. Uploading creates a draft (in_progress). The contractor
explicitly clicks Share (group-scoped) to move work to submitted.
Two publish actions. See in app publishes a version for preview; Done signs it off. Both mark the underlying version "finalized", which is what the runtime SDK serves.
Version
Every upload is an asset_version with a version_number, checksum (sha256),
mime type, size, and an is_placeholder flag. The runtime SDK serves each asset's
most recent finalized version, falling back to a placeholder so the app
always has something. Submitted-but-unpublished uploads are never served.
Access code
A portal's permanent 6-digit code. The runtime SDK uses it (read-only) to fetch the published manifest. Safe to ship in your app.
Schema version
An integer on the portal = the minimum app build required to handle its current assets. Bump it via the CLI when you ship new asset types; the SDK refuses to sync (and tells the user to update) if the portal's version exceeds what the app build supports. See Schema versioning.
Conversations & requests
- Conversations are message threads scoped to a group, an ungrouped asset, or the whole project. Status changes (upload/share/review) post highlighted activity lines into the thread. Image attachments are supported.
- Requests are contractor → developer asks ("add support for sprite atlases") with an open/resolved/declined lifecycle.
Quickstart
This gets a developer from zero to a live portal with assets streaming into a game. (To deploy your own instance of Molta, see Deployment & env.)
1. Create a portal
Log in to the portal, click + New portal, name it. Note its 6-digit access code on the project page.
2. Generate an API token
Project page → CLI / API tokens → Generate. Copy the apt_… value (shown
once).
3. Connect the CLI and seed assets
npm install -g molta # or: node packages/cli/bin/molta.js …
molta login --url https://molta.dev --token apt_xxxxxxxx
molta seed game-assets.manifest.json
This creates sections/groups/assets with briefs + requirements and uploads any placeholder files. See Seed manifest for the file format, or AI seeding to have an AI generate it from your codebase.
4. Invite a contractor
Project page → Team → invite by email. They log in with that email (Auth0) and see their task queue. They upload versions and Share them.
5. Review & finalize
As the developer you'll see "Waiting on your review". Open an asset → See in app to preview live, then Done to sign off (or Request changes / Reject).
6. Stream assets into your game
Add the Swift SDK and connect with the access code:
import MoltaKit
let portal = MoltaClient(
baseURL: URL(string: "https://molta.dev")!,
accessCode: "428193")
let assets = try await portal.sync() // downloads new/changed assets
if let url = portal.localURL(forKey: "hero_ship") { /* load from disk */ }
7. Ship to production
When everything is Done, bake the assets into the binary so production has no network dependency:
molta status # READY / NOT READY
molta bake --out MoltaBaked --require-final
Add the MoltaBaked folder to your app target. Release builds load the
bundled assets; the downloader is compiled out. See Production baking.
Using the portal (web)
Developer workflow
- Create a portal and (optionally) sections and groups.
- Define assets — Add asset with a name, stable
asset_key, type, brief, and JSON requirements. Or bulk-seed via the CLI. - Seed placeholders so the game runs while art is in progress.
- Invite contractors (Team panel) and optionally assign assets/sections.
- Review — the project page surfaces "Waiting on your review" and
"In app — preview & sign off". On an asset:
- 📲 See in app — publish the version live for preview (status
in_app). - ✓ Done — sign off; releases the contractor (status
done). - ↩ Request changes / ✕ Reject — send it back.
- An optional note posts into the conversation with the decision.
- 📲 See in app — publish the version live for preview (status
- Manage tokens (CLI / API tokens panel) and resolve requests filed by contractors.
Contractor workflow
- Log in with the invited email. Your dashboard shows the portal.
- The project page shows "Up next" and a to-do queue of your assets with briefs, requirements, and status.
- Open an asset → Upload a new version (this is a draft — it doesn't notify the developer yet; the row shows a green "✓ draft").
- Share to send for review. Sharing is group-scoped: every asset in the
group must have an uploaded draft before Share enables (
n/total uploaded). - Converse on the asset/group/project, attach screenshots, and file requests for code/app changes you need from the developer.
Conversations
Each asset page (and group page) has a conversation in the right pane; the project page has a portal-wide conversation. Threads are scoped to the group (grouped assets share one), the asset (ungrouped), or the project. Status changes appear as highlighted activity entries. You can attach images.
The right-hand panel (project page)
Top to bottom: Requests to developer, Portal conversation, then (developer only) the access code, team, and CLI / API tokens.
CLI reference
molta is a zero-dependency Node CLI (Node ≥ 18.17, macOS/Linux/Windows)
for seeding, baking, and versioning. It authenticates with a project API token
(apt_…) generated in the portal.
Install & authenticate
npm install -g molta
# or run directly: node packages/cli/bin/molta.js <command>
molta login --url https://molta.dev --token apt_xxxxxxxx
molta whoami
Config is saved to ~/.molta/config.json (mode 600). You can instead set
MOLTA_URL and MOLTA_TOKEN env vars.
Commands
| Command | Purpose |
|---|---|
login --url <u> --token <apt_…> | Validate + save credentials |
whoami | Show the target portal, access code, asset count, schema version |
seed <manifest.json> [--dir <root>] [--dry-run] [--bump | --schema-version <N>] | Upsert sections/groups/assets, upload placeholders, optionally bump version |
push --key <k> --file <p> [--final] | Upload a single asset version |
bake [--out <dir>] [--require-final] | Download published assets to bundle in a production build |
status | Production-readiness report (all assets done? schema version) |
bump-version [--to <N>] | Bump (or set) the asset schema version |
Examples
# Seed from a manifest, resolving placeholder files relative to ./art
molta seed game.manifest.json --dir ./art
# Validate without sending anything
molta seed game.manifest.json --dry-run
# Seed new asset types and bump the schema version in one push
molta seed new-types.manifest.json --bump
# Bake finalized assets for production, failing if any aren't "done"
molta bake --out MoltaBaked --require-final
# Quick CI gate: is everything finalized?
molta status
seed is idempotent (matched by asset_key) — rerun freely after editing the
manifest. See Seed manifest for the file format, Production baking for bake
details, and Schema versioning for --bump / bump-version.
Seed manifest reference
The seed manifest is a JSON file describing the sections, groups, and assets a
portal should contain. molta seed <manifest.json> applies it idempotently.
Full schema
{
// Optional. Set/raise the portal's asset schema version (see Schema versioning).
"schema_version": 2,
// Optional. Loose organizational buckets.
"sections": [
{ "name": "Character Art", "description": "optional" }
],
// Optional. Assets that must ship together (frames, etc.).
"groups": [
{ "name": "Explosion animation", "section": "Character Art",
"description": "frames delivered as a unit" }
],
// Required. One entry per logical asset.
"assets": [
{
"key": "hero_ship", // REQUIRED, lower_snake_case, stable SDK id
"name": "Hero ship sprite", // REQUIRED, human label
"type": "image", // image|video|audio|music|sound|level|model|font|text|data|other
"section": "Character Art", // optional; created if missing
"group": "Explosion animation",// optional group name; created if missing
"description": "What you want — style, mood, references, where it's used.",
"requirements": { // optional, free-form; these keys render nicely:
"width": 256, "height": 256, "format": "png", "max_kb": 120,
"duration_sec": 60, "loop": true, "fps": 30,
"sample_rate": 44100, "channels": 2
},
"placeholder": "placeholders/hero_ship.png" // optional path, relative to --dir
}
]
}
Field rules
key— required, must match^[a-z0-9_]+$, unique within the portal, and permanent (the SDK keys off it; never rename).name— required.type— one of the listed enum values; defaults toother.requirements— any JSON object. The portal renders common keys (width,height,format,duration_sec,loop,max_kb,fps,sample_rate,channels,notes) with friendly labels; others pass through.placeholder— path to a file uploaded as the asset's initial placeholder version. Resolved relative to--dir(or the manifest's folder).group— the contractor must upload all assets in a group before they can share it.
Tips
- Keep
seedidempotent runs the source of truth; re-run after edits. - Use
--dry-runto validate (key casing, types, missing placeholder files) before sending anything. - The schema doc here, the CLI validator (
validateManifest), and the server handler (lib/seed.ts) are kept in sync — all three accept the same shape.
AI seeding
Molta is designed so an AI (e.g. Claude Code) can analyze a game's codebase, extract its assets, write requirements, and seed a portal in one shot.
Prerequisites
The developer has created a portal, generated an API token, and run
molta login (or set MOLTA_URL / MOLTA_TOKEN).
Prompt to give the AI (inside your game repo)
Analyze this codebase and produce an
moltaseed manifest (game-assets.manifest.json). Find every art, audio, music, video, level, and font asset the game loads — search for image/sound loading calls, asset catalogs,Resources/andAssets/folders, and file references in code. For each, emit an object with a stable lower_snake_casekey, aname, the righttype, asection, agroupif frames ship together, adescriptionof what a contractor should produce, andrequirementsinferred from how the asset is used (dimensions from the sprite/atlas, duration/loop for audio, format from the file extension). If a current asset file exists, setplaceholderto its path. Follow the schema in the portal docs (Seed manifest). Then runmolta seed game-assets.manifest.json --dry-runand fix any errors.
What the AI should infer
| Field | How |
|---|---|
key | lower_snake_case, stable, unique. Pick carefully — never renamed. |
type | from usage/extension: image, sound (short SFX), music (loops), level, model, font, … |
section / group | logical grouping; group frames/atlases that ship together. |
description | what a contractor should make — subject, style, references, in-game use. |
requirements | concrete specs: image width/height/format/max_kb; audio duration_sec/loop/sample_rate/channels; video fps. |
placeholder | path to the current asset file, if any. |
Rules of thumb
- One manifest entry per logical asset (dedupe by asset, not file path).
- Keys are forever — choose them deliberately.
- Be specific in
requirements; it's what the contractor is held to. - Re-running
seedis safe and idempotent — iterate freely. - This page itself, plus
/docs/llms.txt, is a single-file source an AI can read to implement an integration end-to-end.
iOS SDK — MoltaKit
The runtime client your game embeds. A dependency-free Swift package (iOS 15+, macOS 12+, tvOS 15+). Other platforms (Android, Unity, web) can implement the same HTTP contract — see API reference.
Install (Swift Package Manager)
Add the repo as a package dependency and add MoltaKit to your target.
Usage
import MoltaKit
let portal = MoltaClient(
baseURL: URL(string: "https://molta.dev")!,
accessCode: "428193", // shown in the portal UI
supportedSchemaVersion: 1) // bump when you add new asset types
// On launch — downloads only what changed (diffed by checksum), caches the rest.
let synced = try await portal.sync()
for asset in synced where asset.didUpdate { print("Updated \(asset.key)") }
// Use any asset by its stable key:
if let url = portal.localURL(forKey: "hero_idle") {
let image = UIImage(contentsOfFile: url.path)
}
if let wav = portal.data(forKey: "laser_shot") { /* feed your audio engine */ }
Development vs production
The over-the-air downloader is for DEV/TEST builds. The behaviour is chosen automatically:
| Mode | sync() behaviour |
|---|---|
.development | Downloads the latest published assets from the portal. |
.production | Loads assets baked into the app bundle; no network. |
.automatic (default) | .development on DEBUG builds, .production otherwise. |
The networking code is compiled out of release builds (#if DEBUG || MOLTA_LIVE), so production binaries contain no download logic. Define
MOLTA_LIVE to force the downloader on in a release build (e.g. a
TestFlight build that should keep updating). See Production baking to exclude
the library entirely in production.
How sync works (development)
GET /api/sdk/manifest(headerX-Access-Code) returns the published assets, each with achecksumand a short-lived signed download URL.- Assets whose checksum differs from the on-disk cache are downloaded into
Caches/Molta/<accessCode>/; the version map is updated. - Unchanged assets are skipped, so repeat launches are cheap and offline-friendly.
Errors
sync() throws MoltaError. Notable case:
.appOutOfDate(required:supported:)— the portal's schema version is higher than this build supports. Show its message and prompt the user to update. See Schema versioning.
What's served
Only assets the developer published via See in app or Done (plus initial placeholders) are served. In-review uploads never reach the app.
Production baking
For a production build you bake the finalized assets into the app so the shipped binary has no network or portal dependency.
Bake
molta status # READY / NOT READY report
molta bake --out MoltaBaked --require-final
bake downloads every published asset and writes into the output folder:
- the asset files,
molta-manifest.json(key → file map + checksums),MoltaBaked.swift— a generated, zero-dependency accessor.
--require-final aborts unless every asset is done, so you never ship a
placeholder or in-review version. Both bake and status print a clear
✅ READY / ⛔ NOT READY FOR PRODUCTION summary.
Two levels of "no network in production"
(a) Keep MoltaKit linked. In release builds the downloader is already
compiled out, so sync() / localURL(forKey:) resolve to the bundled files.
Drag the MoltaBaked folder into your app target (a group, so the
.swift compiles and the files bundle).
(b) Exclude the library entirely. Use the generated accessor and only import MoltaKit in DEBUG:
#if DEBUG
import MoltaKit
let portal = MoltaClient(baseURL: url, accessCode: "428193")
func assetURL(_ k: String) -> URL? { portal.localURL(forKey: k) }
// …call try await portal.sync() on launch
#else
func assetURL(_ k: String) -> URL? { MoltaBaked.url(forKey: k) } // no library
#endif
Link the MoltaKit package only in your Debug configuration and the release binary contains none of it.
The generated accessor
MoltaBaked.swift exposes:
MoltaBaked.keys // [String]
MoltaBaked.url(forKey: "hero_ship") // URL? in the bundle
MoltaBaked.data(forKey: "hero_ship") // Data?
It finds files whether the folder was added as a folder reference or a group.
Schema versioning
Stops an old app build from downloading assets it can't handle. Each portal
has a schema_version — the minimum app build required for its current assets.
The contract
- Server: the portal stores
schema_version(starts at 1). Bump it when you ship asset types that need a newer app. - App: declares the version it was built to understand via
MoltaClient(supportedSchemaVersion:). - Gate: on
sync(), if the portal's version is higher than the app's, the SDK throwsMoltaError.appOutOfDateinstead of serving incompatible assets.
Bumping the server version (CLI)
molta bump-version # +1
molta seed new-assets.json --bump # seed new types AND bump together
molta bump-version --to 5 # set explicitly
whoami and status show the current version. You can also set it declaratively
with a top-level "schema_version": N in the seed manifest.
Declaring the app version (SDK)
let portal = MoltaClient(baseURL: url, accessCode: "428193",
supportedSchemaVersion: 3) // what THIS build supports
Handling the error
do {
try await portal.sync()
} catch let error as MoltaError {
if case .appOutOfDate = error {
showUpdateRequiredAlert(message: error.localizedDescription)
// "This version of the app is out of date — please update to the latest version."
}
}
Typical flow
- You add new asset types in the game → ship a new app build with
supportedSchemaVersion: 4. - Push the new assets and bump:
molta seed new.json --bump(portal → v4). - New app (v4) syncs fine. An old build (v3) hitting the v4 portal gets the "please update" error instead of breaking.
Defaults are back-compatible: existing portals/apps stay at v1 until you bump.
API reference
Three audiences, three auth schemes. All routes are under your portal's base URL.
Versioning. The canonical API surface is /api/v1/.... The unversioned
/api/... paths still work as legacy aliases (so older shipped clients keep
running), but new integrations should use /api/v1. Examples below use /api/v1.
Runtime SDK API (6-digit access code)
Public, read-only. Used by MoltaKit and any other-platform client.
GET /api/v1/sdk/manifest — the published asset manifest.
- Auth: header
X-Access-Code: 123456(or?code=123456). - Returns:
{
"project": "Galaxy Raiders",
"access_code": "428193",
"schema_version": 1,
"generated_at": "2026-06-16T00:00:00Z",
"assets": [
{
"key": "hero_ship", "name": "Hero ship", "type": "image",
"version": 4, "checksum": "abc123…", "size": 20480,
"mime_type": "image/png",
"url": "https://…signed-download-url…",
"metadata": { "width": 256, "height": 256 },
"updated_at": "2026-06-16T00:00:00Z"
}
]
}
Each url is a short-lived signed URL. Diff checksum against your cache and
download only what changed. Only finalized (or placeholder-fallback) versions
appear.
GET /api/v1/sdk/asset/{key} — a single published asset (same auth, same entry
shape).
To implement a client on another platform: call manifest, compare checksum
per key, download changed urls, cache by key. Respect schema_version vs your
build's supported version.
CLI / automation API (Bearer apt_… token)
Project-scoped machine token. Generated in the portal's CLI / API tokens panel.
Header: Authorization: Bearer apt_….
| Method & path | Purpose |
|---|---|
GET /api/v1/cli/project | Project name, access code, asset count, schema version |
POST /api/v1/cli/seed | Upsert sections/groups/assets (+ optional schema_version/bump) |
POST /api/v1/cli/upload | Multipart upload of a placeholder/asset file |
POST /api/v1/cli/versions | Register an uploaded file as a version |
GET /api/v1/cli/bake | Every asset's published version + status + signed URL |
POST /api/v1/cli/schema-version | { bump: true } or { version: N } |
App / web API (Auth0 session or Bearer access token)
Used by the web UI and the iOS companion app. Authenticated by the Auth0 session
cookie (web) or Authorization: Bearer <Auth0 access token> (native). Requires
AUTH0_AUDIENCE to be configured for Bearer.
Selected routes (all enforce membership/role):
GET /api/projects,GET /api/projects/{id},GET /api/assets/{id},GET /api/groups/{id}— JSON reads.POST /api/projects— create a portal.POST /api/projects/{id}/assets·/sections·/groups·/members·/tokens·/requests·/messages·/attachments.POST /api/assets/{id}/upload·/versions·/messages·/share·/status(review actions) ·PATCH/DELETE /api/assets/{id}.POST /api/groups/{id}/messages,POST /api/requests/{id}(resolve/decline).POST /api/devices— register an APNs device token.
Route handlers wrap logic in handler() and throw HttpError(status, msg); errors
return { "error": "…" } with the status.
iOS companion app
A SwiftUI app (apps/ios) so developers and contractors can work from their
phone: browse portals, read & post in conversations with photo/screenshot
uploads, take review actions, file requests, and receive push notifications
for new messages and asset activity.
Build it
brew install xcodegen
cd apps/ios
xcodegen generate # → Molta.xcodeproj (gitignored; always generated)
open Molta.xcodeproj
Xcode resolves the Auth0.swift package automatically.
Configure
- Auth0 — Native application. Create one; note the Client ID + Domain. Add
the callback/logout URLs (Auth0.swift's scheme). Create an Auth0 API whose
identifier matches the portal's
AUTH0_AUDIENCE. Config.swift— fill inportalBaseURL,auth0Domain,auth0ClientId,auth0Audience. Set the bundle id inproject.yml(then re-runxcodegen).- Push (APNs). Create an APNs key (.p8) in the Apple Developer portal and set
the portal env vars
APNS_KEY/APNS_KEY_ID/APNS_TEAM_ID/APNS_BUNDLE_ID/APNS_HOST. The app registers its device token on login.
How it authenticates
The app logs in with Auth0 natively and calls the portal API with the Auth0
access token as a Bearer header — the same routes the web UI uses. (The portal
verifies the token against Auth0's JWKS; see AUTH0_AUDIENCE in Deployment.)
Push
The backend sends a push to the other project members whenever a message is
posted (including upload/share/review activity, which post into the thread) or a
request is filed. Push needs a real device (the Simulator can't receive APNs)
and the APNS_* env configured; otherwise push is simply disabled.
Scope
Full collaboration loop: projects, conversations (project/asset/group) with image attachments, review actions (See in app / Done / request changes / reject), contractor upload + share, and requests. Member admin and CLI-token management remain web-only.
Deployment & environment
The portal is a Next.js 14 app that deploys to Vercel, backed by Auth0 and Supabase.
1. Supabase
- Create a Supabase project; copy the Project URL and service_role key.
- Create a private Storage bucket named
assets. - The schema is applied automatically on deploy (see Database migration below),
or run
supabase/schema.sqlonce in the SQL editor.
2. Auth0
- Create a Regular Web Application (for the web portal). Allowed Callback URL
https://<app>/auth/callback, Allowed Logout URLhttps://<app>. - For the iOS companion app, also create a Native application and an API
(its identifier =
AUTH0_AUDIENCE).
3. Vercel
- Import the repo; set Root Directory to
apps/portal. - Add the environment variables below.
- Deploy. The build runs
vercel-build→scripts/migrate.mjs→next build.
Environment variables
| Variable | Required | Purpose |
|---|---|---|
AUTH0_DOMAIN | yes | Auth0 tenant domain (bare host, no https://) |
AUTH0_CLIENT_ID / AUTH0_CLIENT_SECRET | yes | Web app credentials |
AUTH0_SECRET | yes | openssl rand -hex 32 |
APP_BASE_URL | yes | Canonical URL, e.g. https://app.vercel.app |
AUTH0_AUDIENCE | for iOS app | Auth0 API identifier (enables Bearer auth) |
SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY | yes | Supabase project (server-only) |
SUPABASE_ASSET_BUCKET | no | Storage bucket name (default assets) |
SUPABASE_DB_URL | recommended | Direct Postgres URI → enables build-time migration |
TOKEN_HASH_PEPPER | yes | openssl rand -hex 32 — pepper for API-token hashes |
APNS_KEY / APNS_KEY_ID / APNS_TEAM_ID / APNS_BUNDLE_ID / APNS_HOST | for push | APNs credentials (push disabled if unset) |
Database migration on deploy
If SUPABASE_DB_URL is set (a direct Postgres URI — port 5432, not the
transaction pooler), the build applies supabase/schema.sql idempotently and
ensures the storage bucket exists. Enum additions run as autocommit
pre-migrations. If unset, apply the schema manually in the Supabase SQL editor.
Security notes
- The Supabase service-role key is server-only; never import it into a client
component. Authorization is enforced in app code (
lib/access.ts). - API tokens are stored hashed (sha256 + pepper) and shown once.
- The runtime SDK endpoint exposes only finalized/placeholder versions via short-lived signed URLs.
Data model
Postgres (Supabase). Identity is owned by Auth0; a mirror users row is keyed by
the Auth0 sub. All access is server-side with the service-role key; authorization
is enforced in app code. The source of truth is supabase/schema.sql.
users ──┐
├─< projects ──< sections
│ │ │
│ ├─────< asset_groups
│ │ │
│ ├──────< assets >── section, group
│ │ │
│ │ ├─< asset_versions (current_version_id points here)
│ │ └─< messages (asset thread)
│ ├─< messages (group / project threads)
│ ├─< requests
│ ├─< project_members
│ └─< api_tokens
└─< device_tokens
Key tables
| Table | Notable columns |
|---|---|
projects | access_code (6-digit), schema_version, owner_id |
sections | name, assignee_id |
asset_groups | name, section_id |
assets | asset_key (stable), type, status, requirements (jsonb), group_id, current_version_id |
asset_versions | version_number, status, checksum, is_placeholder, storage_path, metadata (jsonb) |
messages | thread = group_id | asset_id | both null (project); kind (message/event), status, attachment_path |
requests | title, body, status (open/resolved/declined) |
project_members | invited_email, role (developer/contractor), status |
api_tokens | token_hash, prefix (CLI/M2M) |
device_tokens | token, platform (APNs push targets) |
Enums
asset_type: image, video, audio, music, sound, level, model, font, text, data, otherasset_status: placeholder, in_progress, submitted, in_app, done, finalized, rejectedversion_status: submitted, finalized, rejectedmember_role: developer, contractor ·member_status: invited, active, revokedrequest_status: open, resolved, declined
Storage
Asset files and message attachments live in a private Supabase Storage bucket
(assets by default). Paths: projects/{projectId}/assets/{assetId}/v{n}/{file}
and messages/{projectId}/{uuid}.{ext}. The server serves them via short-lived
signed URLs.