Quickstart
You can have a working Scani instance running locally in five minutes. The default stack is completely self-contained — no external accounts, no provider API keys.
Prerequisites
Section titled “Prerequisites”- Bun ≥ 1.3
- Docker (Docker Desktop, OrbStack, or any compatible runtime)
- A free port at: 5173 (SPA), 3011 (api), 8082 (data-provider), 5433 (Postgres), 6380 (Redis), 9000/9001 (MinIO), 1026/8026 (Mailpit).
The five-minute path
Section titled “The five-minute path”-
Clone and seed the env file.
Terminal window git clone git@github.com:MGrin/scani-oss.gitcd scani-osscp .env.example .env -
Install workspace dependencies.
Terminal window bun install -
Boot the full stack.
Terminal window bun run dev:stackThis runs
scripts/sync-env.ts(materialises per-app.envfiles from the root.env) thendocker compose --profile full up -d --build. First boot pulls images and builds the app containers — give it a few minutes. -
Open the SPA.
Terminal window open http://localhost:5173Sign in with a magic-link email. Local emails land in Mailpit at http://localhost:8026 — click the link there.
-
Stop the stack when you’re done.
Terminal window bun run dev:stack:downContainers go away; named volumes (your Postgres data, your MinIO bucket) persist until you
docker compose down -v.
What you get
Section titled “What you get”| Service | URL |
|---|---|
| Frontend SPA | http://localhost:5173 |
| API (tRPC + Elysia) | http://localhost:3011 |
| Data-provider (tRPC) | http://localhost:8082 |
| Postgres | localhost:5433 (scani / scani) |
| Redis | localhost:6380 |
| Mailpit SMTP | localhost:1026 |
| Mailpit UI | http://localhost:8026 |
| MinIO S3 API | http://localhost:9000 |
| MinIO console | http://localhost:9001 (minioadmin / minioadmin) |
What works out of the box
Section titled “What works out of the box”- Magic-link sign-in (emails land in Mailpit).
- Manual holdings with manual prices.
- FX conversion (the Frankfurter provider needs no key).
- Screenshot uploads (stored in the local MinIO bucket).
- The full SPA, dashboard, vaults, groups.
What needs a key
Section titled “What needs a key”These integrations are gated by an API key — without the key, the
corresponding tRPC routes return PRECONDITION_FAILED and the rest of
the app keeps working.
| Provider | Env var | Unlocks |
|---|---|---|
| CoinGecko | COINGECKO_API_KEY | Crypto prices on the paid tier (public tier works without a key but is rate-limited) |
| Finnhub | FINNHUB_API_KEY | Public-equity prices |
| Etherscan | ETHERSCAN_API_KEY | EVM wallet balances (one key covers every EVM chain) |
| Helius | HELIUS_API_KEY | Solana balances + transactions |
| OpenAI | OPENAI_API_KEY | Screenshot parsing |
| Binance OAuth | BINANCE_OAUTH_CLIENT_ID / _SECRET / _REDIRECT_URI | Binance exchange connection flow |
See Optional integration keys for the full list.
Running services on the host instead of in containers
Section titled “Running services on the host instead of in containers”If you prefer to iterate on api / worker / frontend on the host (faster HMR, easier debugger attach) and only run infra in Docker:
docker compose up -d postgres redis mailpit miniobun installbun dev # api + frontend/app concurrentlybun dev:worker # separate terminal — the BullMQ consumerbun dev:data-provider # separate terminal — outbound 3rd-party callsbun run dev:stackCommon gotchas
Section titled “Common gotchas”compose upfails with a name conflict. One-shot init containers (env-sync,deps,minio-init,migrate) exit cleanly but linger with their container names reserved.bun run dev:stack:downremoves them.compose upwithout a priordownwill hit the conflict.- API keys go in
.env, not in compose..envis the single source of truth.scripts/sync-env.tspropagates it to per-app.envfiles. Editingdocker-compose.ymldirectly will drift. ENCRYPTION_KEYmust match between api and worker. They’re the same value in.env.examplefor a reason — if they differ, the worker cannot decrypt the integration credentials the api stored, and every import silently fails.- Persistent data lives in named volumes.
docker compose downpreserves them;docker compose down -vwipes them. The Postgres data, the MinIO bucket, and the Redis AOF all live there. - Two scani checkouts running in parallel share nothing by default.
Compose volumes are scoped per project name (the parent directory),
so each worktree gets its own Postgres / Redis / MinIO data. If you
want them to share, use
--project-nameexplicitly. If you don’t, remember eachbun run dev:stack:down -vonly wipes that worktree’s data.
Going to production
Section titled “Going to production”The dev stack above is the foundation for the production deployment flow at Self-hosting → Production. Two notable differences:
- Migrations are an explicit step. Prod compose ships them as a
profile-gated one-shot (
docker compose --profile migrate run --rm migrate) — you run it on first install and on every upgrade. The dev compose runs them automatically as part ofbun run dev:stack. /api/readyzvalidates schema. The prod compose api healthcheck fails (and frontend-app is blocked) until migrations have been applied. Forgetting to migrate is a loud failure, not a silent one.
Next steps
Section titled “Next steps”- Read What is Scani for the product shape.
- Read Mental model for the domain model.
- Read Tier model when you’re ready to put it behind a real domain.
- Read How to contribute if you’d like to send a PR.