Skip to content

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.

  • 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 (SeaweedFS S3), 1026/8026 (Mailpit).
  1. Clone and seed the env file.

    Terminal window
    git clone https://github.com/MGrin/scani-oss.git
    cd scani-oss
    cp .env.example .env
  2. Install workspace dependencies.

    Terminal window
    bun install
  3. Boot the full stack.

    Terminal window
    bun run dev:stack

    This runs scripts/sync-env.ts (materialises per-app .env files from the root .env) then docker compose --profile full up -d --build. First boot pulls images and builds the app containers — give it a few minutes.

  4. Open the SPA.

    Terminal window
    open http://localhost:5173

    Sign in with a magic-link email. Local emails land in Mailpit at http://localhost:8026 — click the link there.

  5. Stop the stack when you’re done.

    Terminal window
    bun run dev:stack:down

    Containers go away; named volumes (your Postgres data, your SeaweedFS bucket) persist until you run bun run dev:stack:down -- --volumes.

These are the ports the containerised stack publishes. Running the apps on the host instead changes one of them — see below.

ServiceURL
Frontend SPAhttp://localhost:5173
API (tRPC + Elysia)http://localhost:3011
Data-provider (tRPC)http://localhost:8082
Postgreslocalhost:5433 (scani / scani)
Redislocalhost:6380
Mailpit SMTPlocalhost:1026
Mailpit UIhttp://localhost:8026
SeaweedFS S3 APIhttp://localhost:9000
  • 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 SeaweedFS bucket).
  • The full SPA, dashboard, vaults, groups.

These integrations need an API key for full capability — but a missing key is not an error. A provider that boots without its key still registers and degrades: it contributes no rows, or falls back to a keyless public tier. The call succeeds and the rest of the app keeps working. On a stack with no FINNHUB_API_KEY, searching for a US equity returns HTTP 200 with the crypto matches and no equities — nothing anywhere says a key is missing.

The exception is OpenAI: with no OPENAI_API_KEY, starting a screenshot or document parse is refused with PRECONDITION_FAILED, and nothing is queued. Statement files and manual entry still work.

Because the rest degrade silently, check what is configured rather than waiting for an error — every backend prints a provider credentials: line at boot naming the providers that came up unkeyed, and the api serves the same record on /health/deep. See How to tell what’s enabled.

ProviderEnv varUnlocks
CoinGeckoCOINGECKO_API_KEYCrypto prices on the paid tier (public tier works without a key but is rate-limited)
FinnhubFINNHUB_API_KEYPublic-equity prices
EtherscanETHERSCAN_API_KEYEVM wallet balances (one key covers every EVM chain Scani reads)
HeliusHELIUS_API_KEYSolana balances + transactions
OpenAIOPENAI_API_KEYScreenshot parsing

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:

Terminal window
bun run dev:stack:infra # Postgres, Redis, SeaweedFS, Mailpit — nothing else
bun install
bun dev # api + frontend/app concurrently
bun dev:worker # separate terminal — the BullMQ consumer
bun dev:data-provider # separate terminal — outbound 3rd-party calls

On this path the api binds PORT from the root .env — 3001, not the 3011 the table above lists. 3011 is the host port compose publishes for the containerised api; nothing publishes it here. The data-provider (8082) and the SPA (5173) are the same on both paths.

  • The stack fails to start with a container-name conflict. One-shot init containers (env-sync, deps, migrate) exit cleanly but linger with their container names reserved. bun run dev:stack:down removes them; bringing the stack up again without a prior down will hit the conflict.
  • API keys go in .env, not in compose. .env is the single source of truth. scripts/sync-env.ts propagates it to per-app .env files. Editing docker-compose.yml directly will drift.
  • ENCRYPTION_KEY must match between api and worker. They’re the same value in .env.example for a reason — if they differ, the worker cannot decrypt the integration credentials the api stored, and every import silently fails.
  • Drive the stack with the dev:stack commands, not with a bare docker compose. They address two different compose projects. bun run dev:stack names the project after this checkout’s absolute path (scani_<label>_<digest>); a bare docker compose sets no project name, so compose falls back to the directory’s. The two never match, and compose does not complain — a bare docker compose down -v after a bun run dev:stack exits 0, prints nothing and wipes nothing, which reads exactly like success.
  • Persistent data lives in named volumes. bun run dev:stack:down preserves them; bun run dev:stack:down -- --volumes wipes them. The Postgres data, the SeaweedFS bucket, and the Redis AOF all live there.
  • Two scani checkouts run side by side and share nothing. Each derives its own project name, its own volumes and its own host ports, so one checkout’s bun run dev:stack:down -- --volumes cannot reach another’s data. If two of them land on the same host port, see Troubleshooting.

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 -f docker-compose.prod.yml --profile migrate run --rm migrate) — you run it on first install and on every upgrade. The dev compose runs them automatically as part of bun run dev:stack.
  • /api/readyz validates 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.