{"id":50120757,"url":"https://github.com/mgrin/scani-oss","last_synced_at":"2026-05-23T19:00:39.875Z","repository":{"id":359788682,"uuid":"1247493664","full_name":"MGrin/scani-oss","owner":"MGrin","description":"Self-hostable, open-source portfolio tracker for crypto and traditional assets. Same TypeScript codebase as the hosted version, MIT licensed.","archived":false,"fork":false,"pushed_at":"2026-05-23T14:21:06.000Z","size":2788,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-23T14:21:12.409Z","etag":null,"topics":["bullmq","bun","crypto","drizzle","elysia","monorepo","portfolio-tracker","self-hosted","trpc","typescript"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MGrin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":".github/SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["MGrin"]}},"created_at":"2026-05-23T11:47:11.000Z","updated_at":"2026-05-23T14:21:10.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/MGrin/scani-oss","commit_stats":null,"previous_names":["mgrin/scani-oss"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/MGrin/scani-oss","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MGrin%2Fscani-oss","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MGrin%2Fscani-oss/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MGrin%2Fscani-oss/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MGrin%2Fscani-oss/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MGrin","download_url":"https://codeload.github.com/MGrin/scani-oss/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MGrin%2Fscani-oss/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33408490,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-23T18:09:33.147Z","status":"ssl_error","status_checked_at":"2026-05-23T18:09:31.380Z","response_time":53,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bullmq","bun","crypto","drizzle","elysia","monorepo","portfolio-tracker","self-hosted","trpc","typescript"],"created_at":"2026-05-23T19:00:22.301Z","updated_at":"2026-05-23T19:00:39.869Z","avatar_url":"https://github.com/MGrin.png","language":"TypeScript","funding_links":["https://github.com/sponsors/MGrin"],"categories":[],"sub_categories":[],"readme":"# Scani\n\n**Self-hostable, open-source portfolio tracker for crypto and traditional assets.**\n\nOne view across every asset you care about — exchanges, on-chain wallets,\nbrokerages, and manual entries. Same TypeScript codebase runs three ways:\nfully self-hosted, against a hosted data-provider, or as a managed\nservice. MIT licensed.\n\n[![CI](https://github.com/MGrin/scani-oss/actions/workflows/ci.yml/badge.svg)](https://github.com/MGrin/scani-oss/actions/workflows/ci.yml)\n[![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/MGrin/0ecce2153b44eedf13ad350eacb3193d/raw/scani-oss-coverage.json)](https://github.com/MGrin/scani-oss/actions/workflows/coverage.yml)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)\n[![Bun](https://img.shields.io/badge/runtime-Bun-black.svg)](https://bun.sh)\n\n**📚 [Docs →](https://docs.scani.xyz/)** — quickstart, self-hosting,\narchitecture, provider integrations, and the full env-var reference.\n\n---\n\n## Quickstart\n\nYou need [Bun](https://bun.sh) ≥ 1.3 and Docker (Docker Desktop, OrbStack,\nor any compatible runtime).\n\n```bash\ngit clone git@github.com:MGrin/scani-oss.git\ncd scani-oss\ncp .env.example .env\nbun install\nbun run dev:stack        # boots Postgres, Redis, MinIO, Mailpit, api, worker, data-provider, frontend\nopen http://localhost:5173\n```\n\nThe stack is self-contained — no external service credentials required.\nAuth, holdings, FX pricing, and local screenshot storage (via MinIO) all\nwork without any API key. Provider API keys (CoinGecko, OpenAI, exchange\nread-only keys, …) unlock specific integrations.\n\nTo stop:\n\n```bash\nbun run dev:stack:down   # containers down, volumes preserved\n```\n\n## Self-hosting\n\n### Tier model\n\nThe same binaries run three ways. You pick by setting env vars — no\nfeature flags, no code-level switches.\n\n| Tier | Data-provider runs on | Use case |\n|------|----------------------|----------|\n| **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 |\n| **2 — Semi-managed** | A hosted data-provider you point at | You run the api + worker + frontend; a hosted endpoint provides centralized 3rd-party access (CoinGecko, OpenAI, Etherscan, …) without you managing the keys |\n| **3 — Fully managed** | A fully hosted deployment | Someone else runs the whole stack for you |\n\nThe flow between them is just two env vars:\n\n- `SCANI_CLOUD_URL` — where to send outbound 3rd-party requests\n  (`http://data-provider:8082` for Tier 1; a hosted endpoint for Tier 2/3)\n- `SCANI_CLOUD_API_KEY` — the bearer token the api + worker present\n\n### Environment variables\n\nThe full annotated list lives in [`.env.example`](./.env.example). The\nmust-set ones for any real deployment:\n\n| Variable | Purpose |\n|---|---|\n| `DATABASE_URL` | Postgres 16+ connection string |\n| `REDIS_URL` | Redis 7+ connection string |\n| `BETTER_AUTH_SECRET` | 32+ chars; rotates every session if changed |\n| `ENCRYPTION_KEY` | 32 hex chars; must match between api and worker |\n| `JOBS_HMAC_SECRET` | Shared secret for HMAC-gated job admin endpoints |\n| `FRONTEND_URL` / `BACKEND_URL` | What the browser sees; powers CORS + cookies |\n| `S3_*` | Object storage (any S3-compatible store; MinIO locally, R2 / S3 / B2 / … in prod) |\n| `SCANI_CLOUD_URL` / `SCANI_CLOUD_API_KEY` | Where the data-provider lives + bearer to reach it |\n\nOptional integration keys (each one unlocks specific functionality —\nthe corresponding tRPC router returns a `PRECONDITION_FAILED` error\nat call-time if unset):\n\n- `COINGECKO_API_KEY`, `FINNHUB_API_KEY` — pricing\n- `OPENAI_API_KEY` — screenshot parsing\n- `ETHERSCAN_API_KEY` — EVM wallet balances (one key covers all EVM chains)\n- `HELIUS_API_KEY` — Solana balances\n- `BINANCE_OAUTH_CLIENT_ID` / `_SECRET` / `_REDIRECT_URI` — Binance exchange connection\n- `FASTMAIL_API_TOKEN` — magic-link email delivery (or use `SMTP_URL` for any SMTP server)\n\n### Production\n\nThe repo ships a [`docker-compose.prod.yml`](./docker-compose.prod.yml)\nthat pulls pre-built multi-arch images from Docker Hub\n(`scani/api`, `scani/worker`, `scani/data-provider`, `scani/frontend-app`)\nand wires them up with Postgres + Redis + MinIO. One-command bring-up:\n\n```bash\ncp .env.example .env                              # set real values\ndocker compose -f docker-compose.prod.yml up -d\n```\n\nFor a real deployment, set the required env vars in `.env`\n(`BACKEND_URL`, `FRONTEND_URL`, `BETTER_AUTH_SECRET`, `ENCRYPTION_KEY`,\n`JOBS_HMAC_SECRET`, `DATA_PROVIDER_API_KEY`, `SCANI_CLOUD_API_KEY`,\n`LOG_ID_PEPPER`), and put your own TLS-terminating reverse proxy in\nfront of the `frontend-app` container (the only one that needs to be\nreachable from the public internet — nginx inside it proxies `/api`\nand `/ws` to `api` over the compose network).\n\nTo use managed Postgres / Redis / S3-compatible storage, comment out\nthe corresponding services in `docker-compose.prod.yml` and point\n`DATABASE_URL` / `REDIS_URL` / `S3_*` at the managed endpoints.\n\nImages are tagged `:latest` (head of `main`), `:sha-\u003cshort\u003e` (every\npush), and `:1.2.3` / `:1.2` / `:1` (semver tags). Pin\n`SCANI_IMAGE_TAG=1.2.3` in `.env` if you want reproducible deploys.\n\n## Privacy\n\n**Scani's OSS distribution sends no telemetry, ever.** Self-hosted\ninstalls do not phone home: no install ID, no anonymous usage\ncounters, no feature-flag pings, no version-check beacons. The only\noutbound calls a self-hosted stack makes are the ones you explicitly\nconfigure — exchange APIs you connect, the pricing / chain providers\nwhose keys you set in `.env`, and your email transport.\n\nTwo opt-in, default-off exceptions exist:\n\n- **Sentry** (`SENTRY_DSN` / `VITE_SENTRY_DSN`) — error monitoring. No\n  DSN means the SDK is a no-op; nothing leaves the process. Even when\n  enabled, payloads are scrubbed by `packages/business/shared/src/utils/sentry-scrubber.ts`\n  before send.\n- **Whatever you point `SCANI_CLOUD_URL` at** — by default this is the\n  bundled `data-provider` container on the same host. If you point it\n  at a third-party hosted data-provider instead (Tier 2), upstream\n  requests fan out from there. The OSS code makes no such call by\n  default.\n\nWe are not collecting usage analytics for the OSS project itself. We\ndon't plan to. If we ever change our mind, the new feature will be\nopt-in, default-off, fully documented in `.github/SECURITY.md`, and\nshipped as a separate PR you can read end-to-end before deciding.\n\n## Architecture\n\n```\n┌────────────────────────────────────────────────────────────────────────┐\n│  Browser  ──HTTPS──▶  api (Elysia + tRPC)  ──BullMQ──▶  worker         │\n│                            │                             │             │\n│                            └──┬──────────────────────────┘             │\n│                               │ over tRPC                              │\n│                               ▼                                        │\n│                       data-provider                                    │\n│                  (centralized 3rd-party calls:                         │\n│                   CoinGecko, Finnhub, DeFiLlama, OpenAI,               │\n│                   Etherscan, Helius, Google Sheets, …)                 │\n│                                                                        │\n│  Postgres ◀─── api + worker + data-provider (Drizzle)                  │\n│  Redis    ◀─── api (BullMQ producer) + worker (BullMQ consumer)        │\n│  S3       ◀─── worker (screenshot uploads, file imports)               │\n└────────────────────────────────────────────────────────────────────────┘\n```\n\nThree deployable Bun services + one SPA:\n\n- **`apps/backend/api`** — tRPC + Elysia HTTP server. Owns per-user\n  credentialed integrations (exchange API keys, brokerage tokens) so\n  user creds never cross the tenant boundary.\n- **`apps/backend/worker`** — BullMQ consumer. Runs every scheduled\n  job (pricing refresh, balance syncs, historical backfills, transfer\n  linking) and every user-initiated job (screenshot parse, import,\n  delete) in one binary.\n- **`apps/backend/data-provider`** — tRPC service that centralizes\n  outbound 3rd-party calls. The api and worker call it over tRPC rather\n  than reaching for upstream APIs directly. This is the seam between\n  the tiers: in Tier 1 it's on `localhost:8082`, in Tier 2/3 it's a\n  hosted endpoint.\n- **`apps/frontend/app`** — React + Vite SPA. tRPC client end-to-end\n  type-safe with the api.\n\nState splits as you'd expect: Postgres for everything durable (users,\nholdings, transactions, balances, audit log), Redis for BullMQ + the\nper-provider rate-limiter buckets + realtime fan-out, an S3-compatible\nstore for binary uploads.\n\n## Tech stack\n\n- **Runtime**: [Bun](https://bun.sh) (end-to-end — no Node)\n- **Type-check**: [`tsgo`](https://github.com/microsoft/typescript-go) (`@typescript/native-preview`) — 5–10× faster than `tsc` on this monorepo\n- **Lint + format**: [Biome](https://biomejs.dev) (no ESLint, no Prettier)\n- **HTTP**: [Elysia](https://elysiajs.com) + [tRPC](https://trpc.io)\n- **Database**: PostgreSQL via [Drizzle ORM](https://orm.drizzle.team)\n- **Async jobs**: [BullMQ](https://docs.bullmq.io) on Redis, with Postgres advisory locks for cron idempotency\n- **Auth**: [Better-Auth](https://better-auth.com) (sessions in Postgres)\n- **Storage**: S3-compatible via [`@aws-sdk/client-s3`](https://github.com/aws/aws-sdk-js-v3)\n- **Email**: Fastmail JMAP API or any SMTP server\n- **Frontend**: React + Vite + [Tailwind](https://tailwindcss.com) + [shadcn/ui](https://ui.shadcn.com)\n- **Dependency injection**: [typedi](https://github.com/typestack/typedi) (class-field pattern — see [`CLAUDE.md`](./CLAUDE.md))\n- **Testing**: `bun test` with per-test transactional rollback for repository tests\n\n## Integrations\n\nOut of the box, Scani knows how to talk to:\n\n**Exchanges**: Binance, Kraken, Bybit, OKX, Coinbase, KuCoin, Gate.io,\nHTX, Bitfinex, Bitstamp, Crypto.com, Gemini, MEXC, BitMart, Phemex, ProBit\n\n**Brokerages / banks**: Interactive Brokers (Flex Web Service), Wise\n\n**On-chain**: Ethereum + every EVM chain Etherscan V2 supports\n(Polygon, Arbitrum, Optimism, Base, …), Solana (via Helius), Bitcoin,\nTron, TON, ENS\n\n**Pricing**: CoinGecko, Finnhub, DeFiLlama, ExchangeRate-API, Yahoo\nFinance, Google Sheets (for manual-asset prices)\n\n**AI**: OpenAI (screenshot parsing), Perplexity, DeepSeek\n\nEvery provider has a directory under\n[`packages/clients/providers/src/providers/`](./packages/clients/providers/src/providers/)\nwith a typed adapter behind a capability interface. **Adding a new\nprovider is one of the highest-leverage contributions** — see\n[`CONTRIBUTING.md`](./CONTRIBUTING.md).\n\n## Contributing\n\nPull requests welcome — start with\n[`CONTRIBUTING.md`](./CONTRIBUTING.md), then read\n[`CLAUDE.md`](./CLAUDE.md) for the engineering conventions.\n\n**Contributor benefit**: every merged, non-trivial PR earns free permanent\naccess to every paid tier of the hosted Scani service at\n[app.scani.xyz](https://app.scani.xyz). Eligibility and claim flow are\ndocumented in\n[`CONTRIBUTING.md#contributor-benefits`](./CONTRIBUTING.md#contributor-benefits).\n\nHigh-leverage entry points if you're looking for somewhere to start:\n**provider integrations** (new exchanges / brokerages / chains under\n[`packages/clients/providers/`](./packages/clients/providers/)) and\n**translations** (drop a JSON file into\n[`apps/frontend/app/src/i18n/locales/`](./apps/frontend/app/src/i18n/locales/) —\nno other code needs to change, partial translations are accepted, see\n[`locales/CONTRIBUTORS.md`](./apps/frontend/app/src/i18n/locales/CONTRIBUTORS.md)).\n\nSecurity findings should go to **security@scani.xyz**, not a public\nissue. See [`.github/SECURITY.md`](./.github/SECURITY.md) for the full\ndisclosure flow.\n\n## Contributors\n\nThanks goes to these people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):\n\n\u003c!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --\u003e\n\u003c!-- prettier-ignore-start --\u003e\n\u003c!-- markdownlint-disable --\u003e\n\u003ctable\u003e\n  \u003ctbody\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\" valign=\"top\" width=\"14.28%\"\u003e\u003ca href=\"https://github.com/MGrin\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/2393862?v=4?s=100\" width=\"100px;\" alt=\"MGrin\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eMGrin\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/MGrin/scani-oss/commits?author=MGrin\" title=\"Code\"\u003e💻\u003c/a\u003e \u003ca href=\"#maintenance-MGrin\" title=\"Maintenance\"\u003e🚧\u003c/a\u003e \u003ca href=\"https://github.com/MGrin/scani-oss/commits?author=MGrin\" title=\"Documentation\"\u003e📖\u003c/a\u003e \u003ca href=\"#infra-MGrin\" title=\"Infrastructure (Hosting, Build-Tools, etc)\"\u003e🚇\u003c/a\u003e \u003ca href=\"https://github.com/MGrin/scani-oss/pulls?q=is%3Apr+reviewed-by%3AMGrin\" title=\"Reviewed Pull Requests\"\u003e👀\u003c/a\u003e\u003c/td\u003e\n    \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\n\u003c!-- markdownlint-restore --\u003e\n\u003c!-- prettier-ignore-end --\u003e\n\n\u003c!-- ALL-CONTRIBUTORS-LIST:END --\u003e\n\nThis project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome — comment `@all-contributors please add @username for code` (or any other [contribution type](https://allcontributors.org/docs/en/emoji-key)) on any PR or issue.\n\n## Community\n\nQuestions, ideas, and show-and-tell live in\n[GitHub Discussions](https://github.com/MGrin/scani-oss/discussions) —\nthe best place to ask \"is this the right approach\" before you open a PR,\nor to share what you've built on top of Scani. Security issues should go\nthrough the private flow in [`.github/SECURITY.md`](./.github/SECURITY.md)\ninstead of Discussions or public issues. If Scani saves you time and\nyou'd like to fund continued work, [GitHub Sponsors](https://github.com/sponsors/MGrin)\nis open.\n\n## License\n\nMIT. See [`LICENSE`](./LICENSE).\n\n## Roadmap\n\nTracked in [GitHub issues](https://github.com/MGrin/scani-oss/issues).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmgrin%2Fscani-oss","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmgrin%2Fscani-oss","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmgrin%2Fscani-oss/lists"}