Minimal full-stack framework for deploying applications generated by migratis.ai.
- Backend — Django 5 + Django Ninja (REST API) · SQLite (default) or PostgreSQL
- Frontend — React 19 + react-hook-form + i18next
- Docker — all environments
Ports: backend 8004 · frontend 3002
Go to migratis.ai, describe your application, tweak the sandbox until you are happy, then click Generate.
# Backend
cd backend
bash build-docker-local.sh # builds image, runs migrations, starts on :8004
# Frontend (separate terminal)
cd frontend
docker compose up -d # starts on :3002No database configuration needed — SQLite is used by default.
Open http://127.0.0.1:3002/installer, connect with your migratis.ai credentials, select your application and click Install.
The installer will:
- Download and extract the backend Django module
- Activate required framework modules (auth, i18n, etc.) in
settings.py - Register API routers in
api/views.py - Run
migrateand seed translations automatically
The same install machinery is reachable by an autonomous AI agent. An agent that generated an application with its own migratis.ai personal access token already holds the generated ZIP, so it can apply it directly — no human login, cookies or 2FA on the base side:
POST /backend/api/installer/install-package
The body carries the raw generated ZIP plus the same optional install config
({admin, email, stripe}), in any of three shapes — multipart (package file +
config field), JSON ({package_b64, config}), or a raw ZIP body. It runs the
identical pipeline as the UI install: pre-flight Python compile validation (a bad
package is rejected with 422), then the deferred migrate + seed.
Install is asynchronous: a 200 with migrate_deferred: true /
restart_required means the migrate runs on the next autoreload (dev) or restart
(prod) — re-check GET /installer/installed afterwards rather than assuming the
app is live on the 200. In-place upgrades use the sibling
POST /installer/upgrade-package, which returns 409 needs_confirmation with a
row-count preview for destructive changes until the caller resends with
confirm.
Requires
INSTALLER=True. Since these endpoints apply caller-supplied code into the running project, they are gated separately from the UI flow: by default they are loopback-only (only an agent/operator on the same host can reach them). To let a remote agent install, setINSTALLER_AGENT_TOKENin the backend.envand have the agent send it as anX-Installer-Tokenheader; without that header (or from a non-loopback address) the endpoints return403. SetINSTALLER=Falseonce installation is done.
Download the frontend ZIP from the installer result screen, extract it into frontend/src/, then follow the included INSTALL.md to add the new routes to App.js.
docker restart backend-base-api-1
docker exec -it backend-base-api-1 bash
python /backend/manage.py createsuperuserCopy backend/migratis/.env.example and fill in your values. Key variables:
| Variable | Default | Description |
|---|---|---|
USE_SQLITE |
True |
Use SQLite instead of PostgreSQL |
INSTALLER |
True |
Enable/disable the installer (API + page) — see below |
MIGRATIS_BACKEND_URL |
http://host.docker.internal:8000 |
URL of the migratis generator (from inside Docker) |
SECRET_KEY |
— | Django secret key |
ALLOWED_HOSTS |
127.0.0.1,localhost |
Allowed hostnames |
The installer is controlled entirely by the backend with a single setting — there is no separate frontend flag to keep in sync.
In backend/migratis/.env:
INSTALLER=True # installer enabled (default)
INSTALLER=False # installer disabledThen restart the backend:
docker restart backend-base-api-1Behaviour:
INSTALLER=True— the/installer/API is mounted and the/installerpage works normally.INSTALLER=False— the/installer/API is not mounted (its endpoints return404, so they cannot be reached). The/installerpage is still reachable, but it detects the disabled state (via the always-available/installer/statusendpoint) and shows instructions on how to turn it back on, instead of the installer UI.
Once you have installed your application you typically set INSTALLER=False so
the installation API is no longer exposed in production.
PostgreSQL runs as the base-db service behind a Compose profile, so it is
never started in the default SQLite setup (no unused database container).
-
In
backend/migratis/.env, set:USE_SQLITE=False DB_NAME=migratis DB_USER=migratis DB_PASSWORD=change-me # keep in sync with the base-db service DB_HOST=base-db # the base-db service name on the Compose network DB_PORT=5432
-
Rebuild —
build-docker-local.shdetectsUSE_SQLITE=Falseand automatically enables thepostgresprofile, starting thebase-dbcontainer:cd backend && bash build-docker-local.sh
Or start it manually:
cd backend && docker compose --profile postgres up --build
On first boot the entrypoint waits for the database, runs migrate, then
seed_translations — so all translations are repopulated into the fresh
PostgreSQL database automatically.
Carrying over existing data — switching engines points Django at a new, empty database; it does not copy your SQLite rows. Translations come back via re-seeding, but manual edits (and rows added by installed modules) do not. To preserve them exactly, migrate the data while still on SQLite:
docker exec backend-base-api-1 python manage.py dumpdata i18n --indent 2 > i18n_dump.json # after switching and migrating on PostgreSQL: docker exec backend-base-api-1 python manage.py loaddata i18n_dump.json
To switch back to SQLite, set USE_SQLITE=True and rebuild — the base-db
container stays off.