scenique24
v2.0.0
Published
> **This app lives in `demo/` of the combined repository.** It is mounted at > `/demo/` by the combined server (`demo/server/site.js`); the marketing site > is served at `/` from the repo root. Deployment files (`Dockerfile`, > `docker-compose.yml`) ar
Readme
Scenique24 — artist management (vanilla JS + PostgreSQL)
This app lives in
demo/of the combined repository. It is mounted at/demo/by the combined server (demo/server/site.js); the marketing site is served at/from the repo root. Deployment files (Dockerfile,docker-compose.yml) are at the repository root, not here. The instructions below still apply to running the app itself.End users: the User Guide walks through every feature step by step.
Scenique24 is a client-rendered artist-management application built with vanilla JavaScript, Express, and PostgreSQL. It tracks artist and support-staff rosters with availability calendars, projects with assignments and reusable tasks, customers, a shareable company profile, talent and support-staff categories, compensation details, media uploads, and contacts. It includes reports with PDF and CSV exports, per-project PDF reports with grouped artist/crew/costs totals, public artist and support-staff profiles with token-based invitation responses, cross-workspace search, record counters, an English/French interface (English is the default), a demo-site switch (Administrator → Demo site) that disables mail/phone links site-wide and shows demo credentials on the login page, local username/password authentication, and five color themes (Black & White default plus four alternates, each in light and dark).
The app has no build step: the server serves the API and the static frontend from the same process. Pick one of the two setup paths below — Docker (recommended: everything isolated in containers) or native (Node.js + PostgreSQL installed directly).
Requirements
| Path | Needs | |---|---| | Native | Node.js 22+, PostgreSQL 14+ | | Docker | Docker Engine + Docker Compose (v2+); nothing else — Node and PostgreSQL run in containers |
Setup without Docker (native)
Install dependencies and configure the environment:
npm install cp .env.example .envEdit
.envand setDATABASE_URL(andPORTif needed), then create the tables and start the server:npm run migrate # creates the tables npm run dev # development, with file watching # or npm start # productionOptional: fill the database with deterministic demo data (wipes existing rows and
uploads/):npm run seed. Sign in with[email protected]/AdminPass1!(admin) or[email protected]/InvoiceMe12!(regular user).
The first registered user becomes the admin; every user after that is a regular user of their own new organization (see Tenants & users below for how teams share one organization).
Running as a service (native)
Run the app under a process manager so it restarts on failure. Example systemd unit (/etc/systemd/system/scenique24.service):
[Unit]
Description=Scenique24 artist-management application
After=network.target postgresql.service
[Service]
Type=simple
User=scenique24
WorkingDirectory=/srv/scenique24
EnvironmentFile=/etc/scenique24/scenique24.env
ExecStart=/usr/bin/node server/index.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now scenique24
sudo journalctl -u scenique24 -fPut a TLS-terminating reverse proxy (Nginx, Caddy, Traefik) in front and forward all routes to PORT; NODE_ENV=production enables Secure cookies, which requires HTTPS at the proxy.
Setup with Docker
The repository ships a multi-stage Dockerfile and a docker-compose.yml with three services:
| Service | Purpose |
|---|---|
| db | PostgreSQL 16 with a named data volume and a health check |
| migrate | One-shot container that applies db/migrations and exits |
| app | The Scenique24 server (runs as a non-root user), with uploads on a named volume |
Quick start
docker compose up --build -dThen open http://localhost:3000. Migrations run automatically before the app starts. To use a different host port:
APP_PORT=8080 docker compose up --build -d # http://localhost:8080(On Windows PowerShell: $env:APP_PORT = "8080"; docker compose up --build -d.)
The first registered user becomes the admin. Stop everything with docker compose down (add -v to also delete the database and uploads volumes — destructive).
Docker without Compose
If you prefer plain Docker commands, run PostgreSQL and the app separately:
# 1. Database
docker network create scenique24
docker run -d --name scenique24-db --network scenique24 \
-e POSTGRES_USER=scenique24 -e POSTGRES_PASSWORD=scenique24 -e POSTGRES_DB=scenique24 \
-v scenique24-db-data:/var/lib/postgresql/data \
postgres:16-alpine
# 2. Build the app image
docker build -t scenique24 .
# 3. Migrate (once per deployment; re-run after pulling schema changes)
docker run --rm --network scenique24 \
-e DATABASE_URL=postgres://scenique24:scenique24@scenique24-db:5432/scenique24 \
scenique24 node db/migrate.js
# 4. App
docker run -d --name scenique24-app --network scenique24 \
-p 3000:3000 \
-e NODE_ENV=production \
-e DATABASE_URL=postgres://scenique24:scenique24@scenique24-db:5432/scenique24 \
-v scenique24-uploads:/app/uploads \
scenique24Docker notes
- Uploads live in the
uploadsnamed volume (mounted at/app/uploads); database data lives indb-data. Both persist acrossdocker compose downand are only removed bydocker compose down -v. Back both up. - Changing database credentials: edit the
db.environmentblock indocker-compose.ymland theDATABASE_URLvalues to match. The compose defaults (scenique24/scenique24) are for local use only — change the password for anything exposed. - Applying new migrations: after pulling schema changes, run
docker compose build app migrate(ordocker compose build) anddocker compose up -d. The rebuild recreates the one-shotmigratecontainer, so it re-runs before the app starts; already-applied migrations are skipped. Note thatdocker compose up -dwithout a rebuild does not re-run an already-succeededmigratecontainer. - Demo data: run
docker compose exec app node db/seed.jsto wipe and re-seed the database with demo data; this also clears the contents of theuploadsvolume. Dev credentials:[email protected]/AdminPass1!. - Secrets: never bake
.envfiles or credentials into the image — pass them at runtime viaenvironment,--env-file, or an orchestration secret.
Tenants & users
What an organization (tenant) is
Every record in the app belongs to an organization. Data is isolated per organization at the database level (Postgres row-level security) — users of one organization never see another's artists, events, or customers. The organization's name shows read-only on Settings → Defaults.
There are two ways a tenant comes into existence:
- Self-serve signup (shared database). Anyone who completes the sign-up
form (
/signup) automatically creates a new organization named by the Organization name field on that form (falling back to<your name> workspace), and becomes its administrator. Records are private to that organization from the first row. - One instance per customer (recommended for real tenants). Each
customer gets their own deployment: own database, own
uploadsvolume, own subdomain. Deploy the same image withSEED_DEMO=off(skips the demo-data seed — clean database) andDEMO_MODE_DEFAULT=off(the fresh instance stays out of demo mode); migrations run automatically on first boot. See docs/tenant-setup.md for the full per-tenant checklist (database, environment, volume, domain).
Creating and inviting users
How accounts are created depends on the Demo site switch (Administrator page):
| Demo site | Who can create an account | How |
|---|---|---|
| On (demo installs) | Nobody self-serves | Sign in with the demo credentials shown on the sign-in card |
| Off (real deployments) | Anyone via the sign-up form | /signup — but every signup creates a new organization; the first account on a fresh database becomes the administrator, later signups are regular users of their own new org |
To bring teammates into an existing organization, do not sign up again — an administrator invites them:
- Sign in as an administrator and open the Administrator page.
- In Users, fill in the teammate's name, email, a starting password (min 8 characters), and a role (User or Administrator), then Add user. The account is created inside your organization.
- Send the teammate their email + password: they sign in at
/loginand immediately see your organization's records. - Remove takes a user out of the organization and ends their sessions immediately (you cannot remove yourself). Duplicate emails are rejected globally — an email address can belong to only one organization.
Roles: Administrator can open the Administrator page (demo switch, user management); User is a regular workspace member.
Accessing the Administrator page
- In the app: the sidebar's last entry, Administrator (below Activity). It is only usable by admin-role accounts — other roles see an "Administrator access required" notice.
- By URL:
/adminon the app base, e.g.http://localhost:3000/demo/adminin the combined deployment. - Who is an administrator? The first account on a fresh database, the
seeded
[email protected]on demo installs, and anyone invited with the Administrator role (or promoted later by another administrator).
Billing (Stripe, per-seat)
Workspaces are billed by SEATS through Stripe subscriptions, with a per-seat price that follows the band:
| Seats | Price per seat / month | Stripe price env |
|---|---|---|
| 1–2 | $150 | STRIPE_PRICE_SEATS_1_2 |
| 3–5 | $125 | STRIPE_PRICE_SEATS_3_5 |
| 6+ | $100 | STRIPE_PRICE_SEATS_6_PLUS |
Create the three recurring per-seat Prices in Stripe, wire their ids as
env vars (plus STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET), and point
a Stripe webhook endpoint at POST /api/billing/webhook covering
checkout.session.completed, customer.subscription.updated and
customer.subscription.deleted. The workspace admin opens
Administrator → Billing, buys seats via Stripe Checkout, and manages
payment details through the Stripe customer portal; every seat change
flows back through the webhook. Seat limits are enforced on the invite
path (members + open invitations count) — at the limit, invites are
disabled until the subscription is upgraded. Without Stripe env vars the
rest of the app is unaffected (billing routes answer 503).
Deploying updates
The repository includes a GitHub Actions workflow (.github/workflows/deploy.yml) that runs the full test suite against a real PostgreSQL service and, when the tests pass, deploys to the server over SSH.
One-time server setup:
- Install Docker Engine + the Compose plugin, and create a deploy user with SSH key access and
dockergroup membership. - Clone the repository on the server (e.g.
/opt/scenique24). - In the GitHub repository, add the action secrets:
SSH_HOST,SSH_USER,SSH_KEY(the deploy user's private key), andDEPLOY_PATH(e.g./opt/scenique24).
Updating — push to main (or run the workflow manually). The workflow tests the commit, then SSHes in and runs:
git pull --ff-only
docker compose up -d --build
docker image prune -fThe rebuild re-runs migrations before the app restarts; the db-data and uploads volumes persist across deploys. Rollback: git checkout <previous-tag-or-sha> && docker compose up -d --build.
Manual update (no CI, or when the workflow's secrets are not configured): on the server, run the same git pull + docker compose up -d --build yourself.
Moving existing uploads to a fresh server: a new uploads volume starts empty. After the first deploy, copy your local uploads/ contents into it once:
docker compose cp uploads/. app:/app/uploads/Hardening before exposing beyond localhost: move the Postgres password out of docker-compose.yml into environment variables, and put a TLS-terminating reverse proxy (Nginx, Caddy, Traefik) in front of the app port.
Environment
| Variable | Required | Default | Description |
|---|---|---|---|
| DATABASE_URL | yes | — | PostgreSQL connection string, e.g. postgres://postgres:postgres@localhost:5432/scenique24 |
| PORT | no | 3000 | HTTP port; if it is busy the server retries on PORT + 1 |
| NODE_ENV | no | development | Set to production to enable Secure cookies (requires HTTPS) |
| SEED_DEMO | no | — | Set to off on tenant deployments to skip the demo-data seed on boot (clean database; first signup becomes the admin) |
| DEMO_MODE_DEFAULT | no | — | Set to off on tenant deployments so a fresh instance starts out of demo mode (no credentials on the sign-in card, mail/phone links active) |
| JWT_SECRET | yes in production | dev fallback | Signs the auth access tokens (min 32 chars). Required before exposing beyond localhost |
| STRIPE_SECRET_KEY | no | — | Enables subscription billing (per-seat tiers); without it billing routes answer 503 |
| STRIPE_WEBHOOK_SECRET | no | — | Required to verify Stripe webhook signatures once billing is live |
| STRIPE_PRICE_SEATS_1_2 / _3_5 / _6_PLUS | no | — | Stripe Price ids for the three seat bands ($150 / $125 / $100 per seat) |
Native setups read them from a .env file in the project root (see .env.example); Docker setups pass them via environment / --env-file.
Uploads
Uploaded files (headshots, portfolio media, project documents) are stored on local disk in ./uploads/. Set UPLOAD_DIR to override the location (native) or mount a volume at /app/uploads (Docker). Back this directory up alongside the database — the database only stores file metadata and keys, not the file contents.
Tests
The test suite requires a local PostgreSQL server. npx vitest run recreates the scenique24_test database (created automatically) using the admin connection in TEST_ADMIN_URL (default postgres://postgres:postgres@localhost:5432/postgres). Override TEST_DATABASE_URL and TEST_ADMIN_URL if your local PostgreSQL uses different credentials.
An end-to-end API smoke test is also available; it boots the server against its own scratch database and exits 0 when every step passes:
node scripts/smoke.mjsProject layout
| Path | Contents |
|---|---|
| server/ | Express app: HTTP server, auth, sessions, validation, error handling, file storage |
| server/routes/ | One module per resource (artists, projects, assignments, customers, media, …) |
| public/ | Static frontend: HTML entry point and CSS |
| public/js/ | Browser JavaScript: API client, UI components, page controllers, i18n dictionaries (English/French) |
| db/ | SQL migrations and the migration runner |
| tests/ | Vitest suites for the API and shared libraries |
| scripts/ | Maintenance and verification scripts (smoke.mjs) |
| docs/ | Design docs: specs and implementation plans |
| design-system/ | Atelier design-system reference, derived from the shipped CSS |
| .github/workflows/ | CI: test suite on every push, SSH deploy to production |
| Dockerfile, docker-compose.yml | Container build and the db/migrate/app stack |
