Pip (SQLite)
LightestFor Solo developers, homelab, local dev, bare-metal servers, CI
Pure Python. No container runtime required.
Stack
What runs: 1 process
z4j ships one image for z4j. Backend and dashboard are bundled. There is no separate frontend container.
One Python process
z4j serves the FastAPI API, the WebSocket agent gateway, and the React dashboard from a single process. SQLite lives on local disk.
Start the deployment
# Install z4j + umbrella (dashboard, backend, SQLite, all bundled)
pip install z4j
# Start it. First boot auto-mints HMAC secrets, runs migrations, and prints
# a one-time setup URL to stderr. Open the URL to create the first admin.
# z4j auto-detects the server's hostname, FQDN, and LAN IPs for Host-header
# validation. Dev mode still binds to loopback and refuses a LAN bind.
z4j serve
# Open http://localhost:7700 on that host, or use an SSH port forward.
# For a public reverse proxy, configure production explicitly. The persistent
# allowed-hosts file is only for local SQLite names when the env var is unset.
export Z4J_ENVIRONMENT=production
export Z4J_PUBLIC_URL=https://tasks.example.com
export Z4J_ALLOWED_HOSTS='["tasks.example.com"]'
z4j serve
# Useful CLI commands:
z4j check # config + DB reachable (does NOT verify schema head)
z4j migrate current --check-heads # exits non-zero unless the schema IS at head
z4j status # version, DB URL, user/project/agent counts
z4j createsuperuser # provision an admin without the setup URL
z4j changepassword # reset a user password
z4j migrate upgrade head # run alembic migrations explicitly
z4j audit verify # verify the HMAC-chained audit log
z4j reset-setup --force # clear pending pre-first-boot tokens; restart z4j serve to mint a new URL
After z4j is running, open its configured Z4J_PUBLIC_URL. The loopback Compose default is http://localhost:7700.
Confirm everything is wired up
Each framework adapter ships a `doctor` command that probes brain reachability, TLS, WebSocket upgrade, and the on-disk buffer path. Run it as the same user the service runs under.
Django
python manage.py z4j_doctor Flask
python -m z4j_flask doctor FastAPI
python -m z4j_fastapi doctor Bare / Celery / RQ / etc.
python -m z4j_bare doctor
Exits 0 on all-green, 1 on any failure. Add --no-websocket to skip the WS probe, or --json for scripts. The WebSocket probe returns when the background runtime starts and does not wait for authentication, so confirm the agent's connected log and dashboard state too. The doctor catches the common www-data-can't-write-$HOME startup failure (the agent auto-relocates the buffer to $TMPDIR/z4j-{uid} and logs a WARNING). See service-user deployments.
Configure accepted hostnames
z4j validates ordinary host-bearing HTTP requests. The two health endpoints, requests without Host, and agent WebSockets bypass this middleware, so keep equivalent validation at the reverse proxy.
Pip/SQLite auto-detect (env unset)
localhost,127.0.0.1,[::1]- The system hostname + FQDN (incl. Tailscale's
<host>.<tailnet>.ts.net) - Every LAN IP bound on the host (covers
192.168.x.x, Docker bridges, Tailscale)
Local name versus public domain
The persistent file is only read on SQLite when Z4J_ALLOWED_HOSTS is unset. Use it for a missed local hostname. For a public reverse proxy, Docker, or PostgreSQL deployment, configure the environment instead:
# Pip/SQLite local hostname only, with Z4J_ALLOWED_HOSTS unset:
z4j allowed-hosts add tasks.lan
# Public or reverse-proxied deployment:
export Z4J_ENVIRONMENT=production
export Z4J_PUBLIC_URL=https://tasks.example.com
export Z4J_ALLOWED_HOSTS='["tasks.example.com"]'
# Restart the z4j service after changing its environment. Precedence (highest first)
Z4J_ALLOWED_HOSTSenv - pins the list, replaces auto-detect--allowed-hostCLI flag - additive, repeatable~/.z4j/allowed-hostsfile - SQLite only when env is unset- Auto-detect - same SQLite/unset-env path only
Security
Rejected requests get a generic 400 by default in every environment. Local dev can opt into response detail with z4j serve --debug-host-errors; production refuses that flag. Operators always get the detail in the server log via request_id correlation.
Requirements
- Python 3.11 or newer
- No Docker, no Redis, no broker required
- Writes to ~/.z4j/z4j.db (SQLite)
SQLite (auto-created at ~/.z4j/z4j.db)
Single-host evaluation or development; benchmark before production
Is this the right tier for you?
Use this when
- Running z4j on a Raspberry Pi or homelab NUC
- Evaluating z4j locally before committing to a container
- CI jobs that need a task dashboard ephemerally
- Air-gapped Python environments where Docker is not permitted
Not ideal when
- Multiple concurrent admins (SQLite is single-writer)
- Event throughput that has not been validated against SQLite on your host
- Teams with heterogeneous hosts and OS versions
Capabilities in this tier
- All 6 engines supported
- All 3 framework adapters
- Full dashboard UI
- RBAC and audit log
- HMAC wire protocol
- Multi-writer scale-out
- Horizontal brain replicas
How to move up a tier
Move to a Docker preset when you need it. There is no built-in data transfer between installations or database backends, so keep the source and use the API or audit export for records you must retain.
Works with every engine and framework
Framework adapters
Django
Django AppConfig integration, zero boilerplate.
Learn more
Flask
Flask extension pattern. One line to install.
Learn more
FastAPI
Lifespan-hook integration for async stacks.
Learn moreEngine adapters
Celery
The industry standard, covered end-to-end.
Learn more
RQ
Lightweight Redis queue, fully instrumented.
Learn more
Dramatiq
Middleware-driven Dramatiq observability.
Learn more
Huey
Lightweight Redis/SQLite queue, first-class.
Learn more
arq
Async Redis queue for FastAPI-era Python.
Learn more
taskiq
Broker-agnostic async task framework.
Learn moreCompare with
Ready to run z4j with Pip (SQLite)?
Copy the install command above, run it, and open the dashboard on port 7700.