Skip to content

Latest commit

 

History

1,687 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Scani

Self-hostable, open-source portfolio tracker for crypto and traditional assets.

One view across every asset you care about — exchanges, on-chain wallets, brokerages, and manual entries. Same TypeScript codebase runs three ways: fully self-hosted, against a hosted data-provider, or as a managed service. MIT licensed.

CI Coverage License: MIT Bun Bluesky X GitHub Sponsors

Scani in one minute: a narrated walkthrough of the live demo (opens scani.xyz)

▶︎ Watch Scani in one minute → — a narrated walkthrough of the live demo, with captions.

▶︎ Live demo → — a read-only instance with no account, no email and no credential. Seeded with one invented cross-border portfolio (a UK consultant with EUR income, GBP costs, a USD brokerage and crypto on three chains), so every screen has something on it. Every write is refused, so you cannot break it, and it resets itself on a schedule. It is the same scani/frontend-app image docker-compose.prod.yml pulls, so what you click through is what a self-hoster gets.

▶︎ Try it free → — the hosted version. Its app, api and worker are this codebase; billing and analytics are private. Free for a year at every paid tier during the beta preview, no credit card.

📚 Docs → — quickstart, self-hosting, architecture, provider integrations, and the full env-var reference.

🌐 scani.xyz → — what it does, who it is for, and how the three tiers compare.


Quickstart

You need Bun ≥ 1.3 and Docker (Docker Desktop, OrbStack, or any compatible runtime).

git clone git@github.com:MGrin/scani-oss.git
cd scani-oss
bun install
bun run dev:stack        # boots Postgres, Redis, MinIO, Mailpit, api, worker, data-provider, frontend
open http://localhost:5173

The stack is self-contained — no external service credentials required. Auth, holdings, FX pricing, and local screenshot storage (via MinIO) all work without any API key. Provider API keys (CoinGecko, OpenAI, exchange read-only keys, …) unlock specific integrations.

To stop:

bun run dev:stack:down   # containers down, volumes preserved

Need to tweak the dev stack locally (different port, mounted init SQL, custom volume path) without touching the committed compose file? Copy docker-compose.override.yml.example to docker-compose.override.yml — the override file is gitignored and is auto-merged by docker compose on every command.

Screenshots

Dashboard — portfolio overview, allocations, recent activity.

Scani dashboard — portfolio overview

Holdings — every position across every connected source, with cost basis and P/L.

Scani holdings — positions across all connected sources

Money — recurring bills and expected income, in every currency you hold, converted at today's rates.

Scani money — recurring bills and expected income

Accounts — exchanges, wallets, and brokerages grouped by institution.

Scani accounts — exchanges, wallets, and brokerages grouped by institution

Integrations — the 15 credentialed connectors: exchanges, brokerages and banks. Read-only API keys, encrypted at rest. Chains, pricing and AI need no per-user credential and are configured with your own keys in .env.

Scani integrations — connector catalog

Self-hosting

Tier model

The same binaries run three ways. You pick by setting env vars — no feature flags, no code-level switches.

Tier Data-provider runs on Use case
1 — Fully self-hosted The same machine as the rest of the stack (bun run dev:stack) You run everything; ideal for personal use or operators who want full control
2 — Semi-managed A hosted data-provider you point at You run the api + worker + frontend; a hosted endpoint provides object storage, email, OG metadata and token search. You still set your own provider API keys — see below
3 — Fully managed A fully hosted deployment Someone else runs the whole stack for you

The flow between them is just two env vars:

  • SCANI_CLOUD_URL — where to send object storage, email, OG-metadata and token-search requests (http://data-provider:8082 for Tier 1; a hosted endpoint for Tier 2/3)
  • SCANI_CLOUD_API_KEY — the bearer token the api + worker present

Those four are the whole list — they are the only adapters packages/clients/cloud-client/src/ has. Pricing, AI and chain calls do not travel. The api, the worker and the data-provider each boot buildProviderRegistry({ mode: 'direct' }) and call CoinGecko, DeFiLlama, Frankfurter, Finnhub, Etherscan, Helius and OpenAI themselves, so the provider API keys below are read by your api and worker on every tier. Moving to Tier 2 does not remove the need for them.

Environment variables

The full annotated list lives in .env.example. The must-set ones for any real deployment:

Variable Purpose
DATABASE_URL Postgres 16+ connection string
REDIS_URL Redis 7+ connection string
BETTER_AUTH_SECRET 32+ chars; rotates every session if changed
ENCRYPTION_KEY 32 hex chars; must match between api and worker
JOBS_HMAC_SECRET Shared secret for HMAC-gated job admin endpoints
FRONTEND_URL / BACKEND_URL What the browser sees; powers CORS + cookies
S3_* Object storage (any S3-compatible store; MinIO locally, R2 / S3 / B2 / … in prod)
SCANI_CLOUD_URL / SCANI_CLOUD_API_KEY Where the data-provider lives + bearer to reach it

Optional integration keys, read by the api and the worker on every tier. Most degrade silently rather than refusing — a stack missing them comes up green on every health check and then serves worse data, so check the boot line rather than waiting for an error:

  • COINGECKO_API_KEY, FINNHUB_API_KEY — pricing
  • OPENAI_API_KEY — screenshot parsing
  • ETHERSCAN_API_KEY — EVM wallet balances (one key covers all EVM chains)
  • HELIUS_API_KEY — Solana balances
  • BINANCE_OAUTH_CLIENT_ID / _SECRET / _REDIRECT_URI — Binance exchange connection
  • FASTMAIL_API_TOKEN — magic-link email delivery (or use SMTP_URL for any SMTP server)

Every provider that has a keyless branch reports at boot, and the registry logs one summary line whether or not anything is degraded:

docker compose -f docker-compose.prod.yml logs api worker \
  | grep 'provider credentials:'

Production logs are JSON (LOG_PRETTY is forced off when NODE_ENV=production), so the line arrives as a msg field:

{… "mode":"direct","msg":"✅ provider credentials: 5/5 keyed · keyed: coingecko, etherscan, finnhub, openai, solana · degraded: none"}

{… "degraded":["COINGECKO_API_KEY","OPENAI_API_KEY"],"mode":"direct","msg":"⚠️  provider credentials: 3/5 keyed · keyed: etherscan, finnhub, solana · degraded: coingecko [COINGECKO_API_KEY unset → drops to the public rate-limited tier instead of the Pro host]; openai [OPENAI_API_KEY unset → throws on every call, so screenshot and document parsing fail]"}

The degraded line also carries a degraded array of just the unset variable names, which is the cheaper thing to alert on.

The api serves the same record at /health/deep under providerCredentials. An unkeyed provider deliberately does not turn that endpoint red — it is a configuration choice, not an outage.

Production

The repo ships a docker-compose.prod.yml that pulls pre-built multi-arch images from Docker Hub (scani/api, scani/worker, scani/data-provider, scani/frontend-app, plus the opt-in scani/migrate schema runner) and wires them up with Postgres + Redis + MinIO. Two-command bring-up — migrations are explicit, not auto-applied:

cp .env.example .env                                                            # set real values

# Apply schema migrations (do this on first install AND on every upgrade)
docker compose -f docker-compose.prod.yml --profile migrate run --rm migrate

# Bring the long-running services up
docker compose -f docker-compose.prod.yml up -d

See self-hosting → production → Apply migrations for the alternative orchestrators (Kubernetes Job, CI deploy step, standalone docker run) and what the app does if you forget the migrate step (api's /readyz returns 503, worker logs Awaiting schema readiness in a restart loop).

For a real deployment, set the required env vars in .env (BACKEND_URL, FRONTEND_URL, BETTER_AUTH_SECRET, ENCRYPTION_KEY, JOBS_HMAC_SECRET, DATA_PROVIDER_API_KEY, SCANI_CLOUD_API_KEY, LOG_ID_PEPPER), and put your own TLS-terminating reverse proxy in front of the frontend-app container (the only one that needs to be reachable from the public internet — nginx inside it proxies /api and /ws to api over the compose network).

To use managed Postgres / Redis / S3-compatible storage, comment out the corresponding services in docker-compose.prod.yml and point DATABASE_URL / REDIS_URL / S3_* at the managed endpoints.

Images are published on each v* release tag (cut by release-please): :1.2.3, :1.2, :1, and :latest (re-pointed to the most recent release). main pushes are intentionally not a publish trigger — if you need bleeding-edge builds, clone the repo and build locally. Pin SCANI_IMAGE_TAG=1.2.3 in .env for reproducible deploys.

Privacy

Scani's OSS distribution sends no telemetry, ever. Self-hosted installs do not phone home: no install ID, no anonymous usage counters, no feature-flag pings, no version-check beacons. The only outbound calls a self-hosted stack makes are the ones you explicitly configure — exchange APIs you connect, the pricing / chain providers whose keys you set in .env, and your email transport.

Two opt-in, default-off exceptions exist:

  • Sentry (SENTRY_DSN / VITE_SENTRY_DSN) — error monitoring. No DSN means the SDK is a no-op; nothing leaves the process. Even when enabled, payloads are scrubbed by packages/business/shared/src/utils/sentry-scrubber.ts before send.
  • Whatever you point SCANI_CLOUD_URL at — by default this is the bundled data-provider container on the same host. If you point it at a third-party hosted data-provider instead (Tier 2), upstream requests fan out from there. The OSS code makes no such call by default.

We are not collecting usage analytics for the OSS project itself. We don't plan to. If we ever change our mind, the new feature will be opt-in, default-off, fully documented in .github/SECURITY.md, and shipped as a separate PR you can read end-to-end before deciding.

Architecture

┌────────────────────────────────────────────────────────────────────────┐
│  Browser  ──HTTPS──▶  api (Elysia + tRPC)  ──BullMQ──▶  worker         │
│                            │                             │             │
│                            └──┬──────────────────────────┘             │
│                               │ over tRPC                              │
│                               ▼                                        │
│                       data-provider                                    │
│                  (object storage, email, OG metadata,                  │
│                   token search)                                        │
│                                                                        │
│  api + worker ──HTTPS──▶  CoinGecko, Finnhub, DeFiLlama, Frankfurter,  │
│                           OpenAI, Etherscan, Helius, Google Sheets     │
│                           (direct, with their own keys, every tier)    │
│                                                                        │
│  Postgres ◀─── api + worker + data-provider (Drizzle; BullMQ queue)    │
│  Redis    ◀─── rate-limiter buckets + realtime fan-out                 │
│  S3       ◀─── worker (screenshot uploads, file imports)               │
└────────────────────────────────────────────────────────────────────────┘

Three deployable Bun services + one SPA:

  • apps/backend/api — tRPC + Elysia HTTP server. Owns per-user credentialed integrations (exchange API keys, brokerage tokens) so user creds never cross the tenant boundary.
  • apps/backend/worker — BullMQ consumer. Runs every scheduled job (pricing refresh, balance syncs, historical backfills, transfer linking) and every user-initiated job (screenshot parse, import, delete) in one binary.
  • apps/backend/data-provider — tRPC service the api and worker call for object storage, the email transport, Open Graph metadata and token search. This is the seam between the tiers: in Tier 1 it's on localhost:8082, in Tier 2/3 it's a hosted endpoint. It also exposes a chains.* router that has no live caller today. Pricing and AI calls never come here at all — the api and worker make them directly, from their own environment's keys.
  • apps/frontend/app — React + Vite SPA. tRPC client end-to-end type-safe with the api.

State splits as you'd expect. Postgres holds everything durable — users, holdings, transactions, balances, audit log, and the BullMQ job queue in its own bullmq schema. Redis holds the per-provider rate-limiter buckets and realtime fan-out, none of which has to survive a restart. An S3-compatible store holds binary uploads.

Tech stack

  • Runtime: Bun (end-to-end — no Node)
  • Type-check: tsgo (@typescript/native-preview) — 5–10× faster than tsc on this monorepo
  • Lint + format: Biome (no ESLint, no Prettier)
  • HTTP: Elysia + tRPC
  • Database: PostgreSQL via Drizzle ORM
  • Async jobs: BullMQ on its Postgres backend, with Postgres advisory locks for cron idempotency
  • Auth: Better-Auth (sessions in Postgres)
  • Storage: S3-compatible via @aws-sdk/client-s3
  • Email: Fastmail JMAP API or any SMTP server
  • Frontend: React + Vite + Tailwind + shadcn/ui
  • Dependency injection: typedi (class-field pattern — see CLAUDE.md)
  • Testing: bun test with per-test transactional rollback for repository tests

Integrations

Out of the box, Scani knows how to talk to:

Exchanges: Binance, Bitget, Bitstamp, Bybit, Coinbase, Gate.io, Gemini, Huobi, Kraken, KuCoin, MEXC, OKX

Brokerages / banks: Interactive Brokers (Flex Web Service), Wise, Airwallex

On-chain: Ethereum + every EVM chain Etherscan V2 supports (Polygon, Arbitrum, Optimism, Base, …), Solana (via Helius), Bitcoin, Tron, TON. Ethereum addresses are labelled by ENS reverse-resolution; an ENS name is not accepted as input.

Pricing: CoinGecko, Finnhub, DeFiLlama, ExchangeRate-API, Yahoo Finance, Google Sheets (for manual-asset prices)

AI: OpenAI (screenshot parsing), Perplexity, DeepSeek

Every provider has a directory under packages/clients/providers/src/providers/ with a typed adapter behind a capability interface. Adding a new provider is one of the highest-leverage contributions — see CONTRIBUTING.md.

Contributing

Pull requests welcome — start with CONTRIBUTING.md, then read CLAUDE.md for the engineering conventions.

Contributor benefit: every merged, non-trivial PR earns free permanent access to every paid tier of the hosted Scani service at app.scani.xyz. Eligibility and claim flow are documented in CONTRIBUTING.md#contributor-benefits.

High-leverage entry points if you're looking for somewhere to start: provider integrations (new exchanges / brokerages / chains under packages/clients/providers/) and translations (drop a JSON file into apps/frontend/app/src/i18n/locales/ — no other code needs to change, partial translations are accepted, see locales/CONTRIBUTORS.md).

Security findings should go to security@scani.xyz, not a public issue. See .github/SECURITY.md for the full disclosure flow.

Contributors

Thanks goes to these people (emoji key):

MGrin
MGrin

💻 🚧 📖 🚇 👀

This project follows the all-contributors specification. Contributions of any kind welcome — comment @all-contributors please add @username for code (or any other contribution type) on any PR or issue.

Community

Questions, ideas, and show-and-tell live in GitHub Discussions — the best place to ask "is this the right approach" before you open a PR, or to share what you've built on top of Scani. Security issues should go through the private flow in .github/SECURITY.md instead of Discussions or public issues.

Sponsor

Scani is built and maintained by one person in the open. If it saves you time — whether you self-host or use the hosted service — and you'd like to help keep development going, GitHub Sponsors is the way to chip in. Sponsorship funds the ongoing work that keeps the OSS distribution feature-complete and operator-friendly: new provider integrations, performance work, docs, and the time it takes to review every contribution carefully.

License

MIT. See LICENSE.

Roadmap

Tracked in GitHub issues.

About

Self-hostable, open-source portfolio tracker for crypto and traditional assets. Same TypeScript codebase as the hosted version, MIT licensed.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages