Deployment
Deploy ArchiSpark with Docker Compose or Vercel.
Organizations migration (releases including 0018_organizations_expand.sql)
- This release introduces the Organization → Workspace hierarchy via an expand→backfill→verify→contract migration (see Architecture).
- Plan a short maintenance window rather than a rolling update for this release.
- After deploying, run
pnpm --filter @workspace/db backfill:prodonce against the target database (a no-op if already run). - Verify with the three queries in
plan.md's Phase 2 before ever generating0019_organizations_contract.sql(theNOT NULLcontract migration, intentionally not shipped in this release — see that file for the full rationale).
Neo4j schema migrations (production)
packages/db-neo4j(see Neo4j export) ships its own versioned Cypher migrations, separate frompackages/db's Postgres migrations — Neo4j has nodrizzle-kitequivalent.- Migrations are never applied automatically by
apps/serveritself, in any environment (dev, self-hosted, or Vercel) — see how each environment triggers them below and under Vercel and Self-hosted Docker Compose. - Unlike Postgres, a failure here only logs an error and never blocks
startup: Neo4j is a secondary integration (
POST /api/export/neo4j), andgetNeo4jConfig()always falls back to a default URI rather than signaling "unconfigured", so a deployment without Neo4j reachable must still serve requests normally. - Idempotent: already-applied migrations (tracked via
:SchemaMigrationnodes) are skipped, so re-running the migration command after a deployment that adds a newpackages/db-neo4j/src/schema/migrations/*.cypherfile is always safe.
For a manual run against any directly reachable database (local dev, a
dedicated customer database, or Neon's unpooled connection) — same reasoning
as backfill:prod below:
NEO4J_URI=<uri> NEO4J_USER=<user> NEO4J_PASSWORD=<password> \
pnpm --filter @workspace/db-neo4j migrate:prod
# or, with an env file:
pnpm --filter @workspace/db-neo4j migrate:prod /tmp/neo4j-prod.envpnpm migrate (root script) runs this together with the Postgres migration
and backfill in one command. On the archispark Vercel project, the
canonical trigger is the migrate-prod.yml GitHub Actions workflow (see
Vercel below), which runs automatically on every push to main
that adds a migration file.
Vercel
A single Vercel project (archispark, root directory apps/server)
serves the UI, the REST API and the MCP transport — there is no longer a
separate API or MCP-server project. apps/server/vercel.json overrides
buildCommand to build @workspace/env, @workspace/db, @workspace/db-neo4j,
@workspace/auth, and @workspace/image-library first (Vercel's zero-config
Next.js detection doesn't build workspace dependencies on its own).
-
Create the
archisparkproject — import the repo as a Vercel project with root directoryapps/server. -
Add Neon — In Vercel → Storage, add a Neon Postgres database (
archispark), attached toarchispark. Neon auto-injectsDATABASE_URL(pooled) andDATABASE_URL_UNPOOLED(direct). -
Apply database migrations, then the organization backfill using the GitHub Actions workflow Run production migrations (
migrate-prod.yml). It reads theDATABASE_URL_UNPOOLEDrepository secret, so no Vercel environment export is needed on a developer machine. It runs automatically on every push tomainthat touchespackages/db/drizzle-pg/**orpackages/db-neo4j/src/schema/migrations/**(i.e. any merge adding a new migration file), and can also be triggered manually (workflow_dispatch) for a migration that shipped without changing those paths in the same push, or to re-run after a failure.For exceptional local recovery only:
DATABASE_URL="<neon-unpooled>" pnpm --filter @workspace/db migrate:prod
DATABASE_URL="<neon-unpooled>" pnpm --filter @workspace/db backfill:prodbackfill:prod populates workspaces.organization_id/api_tokens.organization_id
(left NULL by the DDL alone) — required once after any migrate:prod run
that includes 0018_organizations_expand.sql or later; a no-op on a fresh
database, and safe to re-run. apps/server never runs it automatically —
migrate-prod.yml's backfill step is what guarantees it always ran before
the first request hits an unbackfilled row. The workflow's Neo4j step (see
Neo4j schema migrations above) exists
for the same reason: nothing in apps/server applies Neo4j migrations either.
-
Set environment variables on
archispark—DATABASE_URL(from Neon, above),KEYCLOAK_URL,KEYCLOAK_REALM,KEYCLOAK_CLIENT_ID_WEB,KEYCLOAK_ADMIN_CLIENT_ID,KEYCLOAK_ADMIN_CLIENT_SECRET,ARCHISPARK_URL, and (only on the pooled realm's deployment)SMTP_HOST/SMTP_PORT/SMTP_USER/SMTP_PASSWORD/SMTP_FROM. Authentication itself (Keycloak realm, client ids/secrets) is configured via the project's Vercel dashboard — see Keycloak login. SMTP config is also detailed in E-mail invitations. -
Redeploy
archispark. -
(Demo project only) Restore demo data —
.github/workflows/seed-demo.yml("Restore demo data") resets and reseedsdemo.archispark.cloud's Neon database, on a daily schedule (0 3 * * *) and via manual dispatch. It reuses the sameDATABASE_URL_UNPOOLEDrepository secret asmigrate-prod.ymlabove — no Vercel environment variable or project configuration is needed for this step. It resets and reseeds local accounts, not Keycloak (noKEYCLOAK_*variables needed for this path), organizations, and workspaces, and deliberately does not touch Neo4j (the export feature isn't enabled on the demo project) — see Restore demo data for the full behavior, including why it's a full wipe rather than a scoped delete.
Self-hosted Docker Compose
.docker/docker-compose.prod.yml is a self-hosted alternative to Vercel: it
runs the published archispark/archispark image (built by
docker-publish.yml from .docker/server/{alpine,trixie-slim}/Dockerfile)
behind Traefik, alongside Postgres, Neo4j, and Keycloak — the single-service
architecture described in Vercel above, self-hosted instead of on
Vercel/Neon.
-
Prepare the environment file —
cp .env.example .env.prod, then set at minimumDB_PASSWORD,NEO4J_PASSWORD,KEYCLOAK_ADMIN,KEYCLOAK_ADMIN_PASSWORD,KEYCLOAK_ADMIN_CLIENT_ID,KEYCLOAK_ADMIN_CLIENT_SECRET,ARCHISPARK_DOMAIN, andKEYCLOAK_DOMAIN(the two domains only used by Traefik's Docker labels —ARCHISPARK_URL/KEYCLOAK_URLremain the full URLs used by the application itself).ARCHISPARK_OS(alpineortrixie-slim) andARCHISPARK_VERSIONselect which published image tag to run. -
Start the stack —
pnpm run prod:up(stop withpnpm run prod:down). Postgres and Neo4j publish no host port; only Traefik's80and443are exposed, so a plain host-sidepnpm migratecannot reach them.apps/servernever applies migrations on its own — afterprod:up, and again whenever a release adds a migration, run:pnpm run prod:migrate # docker compose run --rm migrateThis runs a one-off container from the same published image, on the same Docker network as
postgres/neo4j, applying the Postgres migrations, the organization backfill, and the Neo4j migrations (apps/server/scripts/docker-migrate.mjs). It's excluded fromdocker compose up/prod:up(Composeprofiles: ["tools"]) — naming themigrateservice explicitly viarunis what starts it. -
Enable TLS — uncomment the
certificatesResolversblock in.docker/traefik.yml, setACME_EMAILin.env.prod, and addentrypoints=websecure/tls.certresolver=letsencryptto each router's labels (already set by default on theserverandkeycloakrouters indocker-compose.prod.yml). -
Review the imported Keycloak realm —
keycloak.realm-export.jsonis the same realm used for local development (registrationAllowed: false,redirectUrispointing atlocalhost). Adjust it for the real domain before the first deployment, or provision a dedicated realm instead withsetup:realmbelow.
Contact form (SMTP)
The landing page's contact form (apps/docs/app/(home)/page.tsx, section
#contact) posts to apps/docs/app/api/contact/route.ts, which sends the
message with nodemailer through the same SMTP_* variables as apps/server's
invitation mail (see E-mail invitations) — one
SMTP account (e.g. an OVH mailbox) for the whole monorepo, plus
CONTACT_TO_EMAIL for the inbox that receives submissions.
Set SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM, and
CONTACT_TO_EMAIL on the apps/docs Vercel project (root directory
apps/docs). Without SMTP_HOST or CONTACT_TO_EMAIL, /api/contact
returns 502 and the rest of the landing page keeps working. A hidden
honeypot field in the form silently discards bot submissions before they
reach the SMTP server.
Onboard a new customer with a dedicated Keycloak realm
ArchiSpark can run as a dedicated platform per customer: a separate
apps/server deployment and PostgreSQL database use a shared self-hosted
Keycloak. Customer isolation comes from Keycloak realms: each customer has a separate
archispark-<tenant> identity namespace for users, roles, identity providers,
JWKS, and issuer. See One Keycloak realm per
client.
Onboarding requires configuration only; no application code changes.
-
Create the customer realm with the existing
packages/db/scripts/setup-realm.tsscript. It does not assume a fixed realm name:KEYCLOAK_URL=<shared self-hosted Keycloak URL> \ KEYCLOAK_REALM=archispark-<tenant> \ KEYCLOAK_SETUP_AUTH_REALM=master \ KEYCLOAK_SETUP_USERNAME=<master administrator> \ KEYCLOAK_SETUP_PASSWORD=<password> \ pnpm --filter @workspace/db setup:realmAlternatively, use
KEYCLOAK_SETUP_AUTH_REALM=archispark-<tenant>and an administrator of that realm when the account cannot accessmaster.Do not set
KEYCLOAK_SELF_REGISTRATIONorKEYCLOAK_VERIFY_EMAILfor a dedicated realm. Omitting them leaves the realm configuration unchanged, withregistrationAllowed: falseby default. They are intended only for the pooled realm described under E-mail invitations. -
Retrieve the secret for the generated
archispark-apiservice account from Admin Console → Clients →archispark-api→ Credentials. -
Provision the dedicated PostgreSQL database, apply migrations with
pnpm --filter @workspace/db migrate:prod, then runpnpm --filter @workspace/db backfill:prod. The backfill is a no-op on a new database; see Organizations migration). -
Deploy the customer's
archisparkwith Vercel and point it at the shared Keycloak. ConfigureDATABASE_URLfor the customer database,KEYCLOAK_URLfor shared Keycloak,KEYCLOAK_REALM=archispark-<tenant>,KEYCLOAK_CLIENT_ID_WEB=archispark-web,KEYCLOAK_ADMIN_CLIENT_ID/KEYCLOAK_ADMIN_CLIENT_SECRET. -
Optionally seed initial accounts:
KEYCLOAK_REALM=archispark-<tenant> pnpm --filter @workspace/db seed:demo-users, or create real users through the Keycloak Admin Console or API. -
Optionally configure customer SSO under Admin Console → Identity providers. Google, Microsoft Entra ID, and other OIDC or SAML providers are scoped to this realm and invisible to other customers.
-
Test sign-in end to end and verify isolation. A token issued by one customer realm must receive
401from another customer's deployment;verifyAccessTokenrejects the mismatched issuer. See One Keycloak realm per client).
E-mail invitations (SMTP)
The pooled SaaS realm, unlike a dedicated customer realm, enables Keycloak self-registration and e-mail invitations. See Organization invitations by e-mail. Two sets of variables share one SMTP service:
KEYCLOAK_SELF_REGISTRATION=true,KEYCLOAK_VERIFY_EMAIL=true, andKEYCLOAK_RESET_PASSWORD=trueare passed topnpm setup:realmonly for the pooled realm. When absent, configuration remains unchanged, as required for dedicated realms.SMTP_HOST,SMTP_PORT,SMTP_STARTTLS,SMTP_USER,SMTP_PASSWORD, andSMTP_FROMare used byapps/serverfor invitation mail, by Keycloak for address verification and password reset, and byapps/docsfor its contact form. When Keycloak reaches the SMTP server from a different network, setKEYCLOAK_SMTP_HOSTto its network-visible host; it overridesSMTP_HOSTonly in the realm configuration. Local development useslocalhost:1025forapps/server,mailpit:1025for Keycloak, and exposes the captured messages athttp://localhost:8025.- Invitation delivery is selected for each create or resend operation in the
member-management interface: email and copyable link, email only, or link
only. API clients pass the equivalent
delivery_modevalue (both,email, ormanual); it defaults toboth. The clear link is never stored and cannot be retrieved later. - The Keycloak service account needs
manage-users,view-users, andquery-users: ArchiSpark searches identities by exact e-mail, provisions a missing account without credentials, and invokesexecute-actions-emailwithUPDATE_PROFILE,UPDATE_PASSWORD, andVERIFY_EMAIL. The action e-mail redirects back to the original ArchiSpark invitation. - An air-gapped setup also needs the pnpm dependencies and Docker images to
have been downloaded beforehand. Provision Keycloak users through its local
admin console or
pnpm run seed:demo-users. ARCHISPARK_URLis the public deployment URL used to build${ARCHISPARK_URL}/invitations/<token>. It is never inferred from the request'sHostheader.
Configure these variables on the archispark Vercel project.