Deployment.
Where each part runs in production.
Deploying means putting each part on a server so your users can reach it. The backend deploys to your Firebase project with one command. The console needs a small Node 22 host with HTTPS. The web build is optional and meant for demos. This page covers each, plus your own API domain.
01Backend (Cloud Functions)
cd firebase
firebase deploy --only firestore:rules,firestore:indexes,storage,functions
Terminal: macOS or Linux as shown; on Windows use WSL or Git Bash (how).
Deploys four functions: api (HTTPS, 1 GiB, 300 s timeout, streaming, public invoker; it checks Firebase ID tokens itself), sendCampaigns (every 5 minutes), refreshDiscover (every 10 minutes; does nothing while Discover is off) and cleanup (daily 03:30 UTC), together with the Firestore and Storage rules and indexes. The predeploy step builds the TypeScript. Re-run the same command after every update. The first deploy enables the Google Cloud APIs it needs, which can take a few minutes; if the CLI asks to enable an API, accept.
Region: FUNCTIONS_REGION in firebase/functions/.env (default us-central1). Do not forget the signed URL permission.
02Your own API domain (optional)
With Firebase Hosting you can serve the API at https://your-domain/api. Add a rewrite to firebase/firebase.json:
"hosting": {
"public": "public",
"rewrites": [{ "source": "/api/**", "function": { "functionId": "api", "region": "us-central1" } }]
}
Terminal: macOS or Linux as shown; on Windows use WSL or Git Bash (how).
Create the folder firebase/public (it may hold a simple index.html), use your region, deploy with firebase deploy --only hosting, connect the domain in Firebase console → Hosting, and build the app with --dart-define=API_BASE_URL=https://your-domain/api. Any prefix before /v1 is ignored by the API. If you put a CDN or proxy in front, make sure it does not buffer text/event-stream responses (streaming).
03Console on a Node 22 server
Any VPS, Render, Railway, Fly or a container. One long-running instance with a persistent disk:
cd admin
npm ci
npm start # compiles src/ to dist/, then runs it with plain node
Terminal: macOS or Linux as shown; on Windows use WSL or Git Bash (how).
- Set the environment (Configuration): at least
ADMIN_SECRET_KEYandGOOGLE_APPLICATION_CREDENTIALS(or the attached service account on Google Cloud). - The console keeps its own data (team, sessions, settings, history, audit log, encrypted keys) in
data/admin.sqlite. Putdata/on a persistent volume and back it up together withADMIN_SECRET_KEY. - Run one instance with SQLite.
- Put HTTPS in front (your host's TLS, or a reverse proxy such as Caddy or nginx). The session cookie is Secure over HTTPS; the server honours
x-forwarded-protofrom the proxy. - Open
https://your-console-host/admin.
04Console with Docker
cd admin
docker build -t museai-admin .
docker run -p 8790:8790 -v museai-admin-data:/app/data --env-file .env museai-admin
Terminal: macOS or Linux as shown; on Windows use WSL or Git Bash (how).
The image is node:22-bookworm-slim, runs as the node user and keeps /app/data in a volume. If you mount the service-account file into the container, point GOOGLE_APPLICATION_CREDENTIALS at its path inside the container.
05Console on serverless or several instances
Set DATABASE_URL (a pooled Postgres URL with sslmode=require) and ADMIN_SECRET_KEY. The console then keeps its data in Postgres tables named mk_museai_* instead of the SQLite file, so any instance can answer.
06Web build (optional)
The web build is meant for a public demo next to its API:
cd app
flutter build web --release --csp --no-web-resources-cdn --dart-define=DEMO_MODE=true --dart-define=API_BASE_URL=/api
Terminal: macOS or Linux as shown; on Windows use WSL or Git Bash (how).
It expects the API at /api on the same host. A full web app with your Firebase project (sign-in, backup) is possible with flutterfire configure for web and a normal API_BASE_URL, but ads, store purchases and video export do not exist on the web; the app is designed for Android and iOS.
Hosting a public demo (optional)
A demo needs no Firebase project: firebase/functions/src/api/vercel.ts exports vercelHandler, a web-standard fetch handler that runs the same API with an in-memory store and the sample configuration (demo-config.ts). Without AI keys it answers with labelled demo replies and a labelled sample picture. Any host that runs a Node 22 fetch handler works; for Vercel, on your own account:
- Entry fileCreate
api/muse.tsin an empty folder next to your web build:import { vercelHandler } from "<path to>/firebase/functions/src/api/vercel"; export default { fetch: vercelHandler }; - Bundle itFrom
firebase/functions(afternpm ci):npx esbuild <folder>/api/muse.ts --bundle --platform=node --format=esm --target=node22 --outfile=<folder>/api/muse.mjs - Copy the web buildCopy
app/build/web/into the folder. - Route and protectIn
vercel.json, rewrite/api/v1/:path*to/api/muse, every other path to/index.html, and send the Content-Security-Policy below for the web app. Optional env vars on the host for real answers:DEMO_AI_PROVIDER,DEMO_AI_KEY,DEMO_AI_MODEL(andDEMO_IMAGE_PROVIDER,DEMO_IMAGE_KEY,DEMO_IMAGE_MODEL); the demo caps every visitor (15 messages and 3 images a day). - Deploy
vercel deploy --prodfrom the folder.
Serve the web app with a strict policy, without 'unsafe-inline' and without 'unsafe-eval'. That works because the app is built with --csp and --no-web-resources-cdn (CanvasKit is served from the same host). 'wasm-unsafe-eval' lets CanvasKit's WebAssembly start, the two sha256 hashes are the style blocks Flutter 3.41 injects at start, and fonts.gstatic.com serves fallback fonts. The policy the demo uses:
default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'sha256-2hxujCU6WzSO6ycZznxFKlHE++BDsDuMYXT/O0zl8rM=' 'sha256-cgINIZw4iKgU7zccO2vPeZdQJIMlKEchLZG6hpkP+8k='; connect-src 'self' https://fonts.gstatic.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob:; media-src 'self' blob: data:; worker-src 'self' blob:; frame-ancestors 'none'; base-uri 'self'; form-action 'none'; object-src 'none'After a Flutter upgrade, recompute the two hashes from the browser console message "Refused to apply inline style". The console and these docs send their own policies.
07After deploying
curl <API_BASE_URL>/v1/healthanswers"ok":trueand"ai":"ok".- Console → Status shows no blockers.
- Google Cloud Billing has a budget alert; each AI provider has a spending limit.
ADMIN_SECRET_KEY, the service-account file, the keystore andkey.propertiesare backed up somewhere safe and are not in git.