atelier-api — the backend
atelier-api is the server behind the app: login, team cloud, storage and building together. One instance is enough for the whole team. This page shows you how to self-host it.
Overview
atelier-api is a lean Bun service with MongoDB as its database. Assets live in a content-addressed store on disk (addressed by SHA-256, deduplicated). By default the server listens on port 3095.
- Discord login with an approval workflow (pending → approved).
- Versioned pack revisions, resumable uploads, team locks over WebSocket.
- A registry lane with a service token so websites can query published packs.
- Web admin dashboard at
/admin— logs, storage, build downloads and fxmanifest overrides.
Source & issues: atelier-api on GitHub.
Requirements
- Bun (1.x) — to run the server.
- MongoDB — your own instance or MongoDB Atlas.
- A Discord application for login (free in the Discord Developer Portal).
- Persistent storage for the assets — see Storage & volume.
Run it locally
cd atelier-api bun install cp .env.example .env.local # fill in the values bun run dev # with auto-reload # Running? Health check: curl http://127.0.0.1:3095/health
Environment variables
Configuration goes through .env.local (locally) or your host's environment. The most important variables:
| Variable | Description |
|---|---|
| MONGODB_URI | Required — connection string to MongoDB (e.g. mongodb+srv://…). |
| MONGODB_DB_NAME | Database name (your choice), defaults to atelier. |
| ATELIER_JWT_SECRET | Required — secret for signing tokens (long & random). |
| ATELIER_SERVICE_TOKEN | Required — token for the service lane (registry for websites). |
| ATELIER_PUBLIC_ORIGIN | Public base URL of the server (https://… in production). |
| ATELIER_DISCORD_CLIENT_ID | Client ID of your Discord application. |
| ATELIER_DISCORD_CLIENT_SECRET | Client secret of your Discord application. |
| ATELIER_ADMIN_DISCORD_IDS | Comma-separated list of Discord IDs that are admins automatically. |
| ATELIER_STORAGE_ROOT | Path for the assets — must be persistent (default ./data). |
| PORT | HTTP port, defaults to 3095. |
| HOST | Bind address — 0.0.0.0 in production. |
| ATELIER_BUILD_CONCURRENCY | Parallel server builds, defaults to 2. |
Never commit secrets
ATELIER_JWT_SECRET and ATELIER_SERVICE_TOKEN should be long and random (e.g. openssl rand -hex 32) and belong only in the environment — never in the repo. The full list including defaults lives in .env.example.Discord OAuth & access
Create a Discord application
- 1In the Discord Developer Portal create a new application (name it e.g. "atelier").
- 2Under OAuth2 copy the client ID and client secret into your env (
ATELIER_DISCORD_CLIENT_ID/ATELIER_DISCORD_CLIENT_SECRET). - 3Add both redirects exactly — one for the desktop app, one for the web admin dashboard:Locally that's
{ATELIER_PUBLIC_ORIGIN}/api/v1/auth/discord/callback {ATELIER_PUBLIC_ORIGIN}/admin/callbackhttp://127.0.0.1:3095/api/v1/auth/discord/callbackandhttp://127.0.0.1:3095/admin/callback. Theidentifyscope is requested automatically.
Approval workflow
New members start out as pending and have to be approved once. Discord IDs in ATELIER_ADMIN_DISCORD_IDS are admins and approved automatically — those admins then approve everyone else directly in the app.
pending— signed in, but blocked from cloud actions.approved— full access to packs, sync and builds.locked— blocked, all device sessions are revoked.
Storage & volume
Every uploaded file lives under ATELIER_STORAGE_ROOT in a content-addressed store. MongoDB only holds the metadata and references (by SHA-256).
cas/ finalized assets (immutable, by hash) tmp/ uploads in progress builds/ cached build artifacts
The volume MUST be persistent
asset_not_found. Always put ATELIER_STORAGE_ROOT on a persistent volume and back it up regularly. After a wipe, owners have to re-upload their packs.Deployment (Dokploy)
atelier-api ships with a Dockerfile (oven/bun) so it runs as a container anywhere. With Dokploy, in short:
- 1In Dokploy, create a new application from the atelier-api repository (or the Docker image).
- 2Set the environment variables — in particular
MONGODB_URI,ATELIER_JWT_SECRET,ATELIER_SERVICE_TOKEN, the two Discord values andHOST=0.0.0.0. - 3Mount a persistent volume at
ATELIER_STORAGE_ROOT(e.g./data). This is the most important step — without it you lose all assets on the next deploy. - 4Assign a domain and expose port
3095. Set the same public HTTPS URL asATELIER_PUBLIC_ORIGINand register it as a Discord redirect. - 5Deploy and verify with
GET /health.
docker run -d --name atelier-api \ -p 3095:3095 \ -v atelier-data:/data \ --env-file .env \ atelier-api
Admin dashboard (web)
The server ships its own web dashboard at /admin — sign-in is limited to the Discord IDs in ATELIER_ADMIN_DISCORD_IDS. Sign-in runs through a dedicated Discord web flow (separate from the desktop app), the session lives in an HttpOnly cookie, and admin status is re-checked on every request.
- Overview — storage size (CAS/builds/tmp) and key figures (assets, packs, revisions, builds, users).
- Logs — server logs live (SSE) plus an activity log.
- Packs & builds — generate or rebuild server builds per revision and download the finished packages as ZIP.
- fxmanifest — override the resource name and an
fxmanifest.luatemplate per pack. It takes effect on the next server build; without an override everything stays byte-identical to the desktop build. - Users — approve and lock.
Prerequisite
{ATELIER_PUBLIC_ORIGIN}/admin/callback (see Discord OAuth). After that it's reachable at {ATELIER_PUBLIC_ORIGIN}/admin.Endpoint overview
All endpoints live under /api/v1. User endpoints need a bearer token (from login), the registry lane needs a service token. The main groups:
| Group | Purpose |
|---|---|
| auth / device | Discord login, token exchange and renewal, logout. |
| me / devices | Manage your own profile and signed-in devices. |
| admin | List members, approve, lock, set roles. |
| /admin (web) | Browser dashboard: overview, logs, build downloads, fxmanifest — cookie login, admins only. |
| uploads / assets | Resumable chunk uploads and asset download (with Range/ETag). |
| packs / revisions | Packs, members and versioned revisions. |
| locks | Advisory locks per drawable (TTL, heartbeat). |
| builds | Queue server builds and fetch artifacts. |
| registry | Service lane: published packs for websites (service token). |
| ws | WebSocket for presence, locks and live status. |
Health & service token
GET /health. Websites talk to the registry via the x-fg-service-token header. Server builds contain no binary YMTs (those are produced in the desktop build) — they serve preview and distribution.