Skip to content

Repo layout

Bun workspaces monorepo. Apps split into backend/ and frontend/; packages split into business/, infra/, clients/, and frontend/.

  • Directoryapps/
    • Directorybackend/
      • Directoryapi/ tRPC + Elysia HTTP server. BullMQ producer.
      • Directoryworker/ BullMQ consumer. Every async job runs here.
      • Directorydata-provider/ Centralised outbound 3rd-party calls.
    • Directoryfrontend/
      • Directoryapp/ React + Vite SPA.
      • Directorydocs/ This Starlight docs site.
  • Directorypackages/
    • Directorybusiness/ Domain logic + the wire contract.
      • Directorydomain/ Services, repositories, use cases.
      • Directoryjobs/ Async-job catalog: descriptors, schedules.
      • Directoryshared/ Frontend-safe contract (zod DTOs, Decimal).
    • Directoryinfra/ System concerns. No business knowledge.
      • Directorydb/ Drizzle schema, migrations, BaseRepository.
      • Directoryqueue/ BullMQ framework (queue, worker, scheduler).
      • Directoryemail/ Fastmail JMAP / SMTP.
      • Directorylogging/ Structured logging (pino).
      • Directorysecurity/ AES-256-GCM credential encryption.
      • Directorystorage/ S3-compatible object storage.
      • Directoryrealtime/ SSE / Redis pub-sub.
      • Directoryrate-limiter/ Per-provider rate limit + circuit breakers.
      • Directoryconfig/ Env-validation primitives.
    • Directoryclients/ Outbound network adapters.
      • Directoryproviders/ Unified 3rd-party integration package.
      • Directorycloud-client/ Typed tRPC client for the data-provider.
    • Directoryfrontend/
      • Directoryui/ Design system + shared client plumbing (@scani/ui).
  • Directoryscripts/ Workspace scripts (sync-env, …).
  • docker-compose.yml Local dev stack.
  • docker-compose.prod.yml Production-flavour compose.
  • .env.example Annotated env vars.
  • CLAUDE.md Canonical engineering spec.
  • CONTRIBUTING.md Contributor on-ramp.

tRPC over Elysia. Owns per-user credentialed integrations (exchange API keys, brokerage tokens). The api decrypts and uses those credentials on its own machine; they never leave the tenant boundary. Acts as the BullMQ producer — every user action that needs async work enqueues into Postgres from here.

Routes live in apps/backend/api/src/presentation/routers/.

The BullMQ consumer. Every scheduled job (pricing, balance syncs, historical backfills, transfer linking, APY payouts, …) and every user-initiated job (screenshot parse, exchange import, wallet import, file import, …) runs here in one binary.

Processors live in apps/backend/worker/src/processors/. The cron-lock helper is in apps/backend/worker/src/lib/cron-lock.ts.

tRPC service fronting the Scani-owned third-party calls that a hosted deployment can serve on your behalf: object storage, the email transport, Open Graph metadata and token search. This is the seam between tiers — in Tier 1 it’s a sidecar on localhost:8082; in Tier 2/3 it’s a hosted endpoint.

It also exposes pricing.*, ai.* and chains.* routers, but the api and worker do not call them: all three services boot the provider registry in direct mode and reach CoinGecko, Finnhub, DeFiLlama, OpenAI, Etherscan and Helius themselves, with keys from their own environment.

Routers live in apps/backend/data-provider/src/presentation/.

The main React + Vite SPA. End-to-end type-safe with the api via tRPC. Source under src/v2/. Locales under src/i18n/locales/.

This site. Astro + Starlight. Pages under src/content/docs/. Custom CSS in src/styles/custom.css. scripts/generate-llms-txt.ts runs before astro build to emit public/llms.txt and public/llms-full.txt.

Things that change when the product changes.

The bulk of business logic — services, repositories, use cases. Class-field DI (see DI pattern). Tests in mirrored tests/ directories.

Per-job descriptors (payload schemas, retry policies, jobId strategies, summarisers), repeatable schedules, and the @scani/queue mirror/lock impls. The wire-name catalogue is job-names.ts.

Frontend-safe contract: zod DTOs (the tRPC wire), the project-configured Decimal.js instance, UI helpers (formatCurrency, formatRelative, emailSchema, …). Strict rule: no Node-only APIs reachable from the barrel. The frontend must be able to import any of it.

Pure system concerns. No business knowledge; reusable in any TypeScript backend.

PackageRole
@scani/dbDrizzle schema, migrations, postgres.js connection, BaseRepository.
@scani/queueAsync-coordination framework on BullMQ. Abstract bases + concrete @Service() impls.
@scani/emailFastmail JMAP / SMTP. Owns FASTMAIL_API_TOKEN / SMTP_URL.
@scani/loggingStructured logging (pino). Owns LOG_*.
@scani/securityAES-256-GCM credential encryption. Owns ENCRYPTION_KEY.
@scani/storageS3-compatible object storage. Owns S3_*.
@scani/realtimeRealtime / SSE pub-sub via Redis.
@scani/rate-limiterRate limiting + circuit breakers for upstream calls.
@scani/configEnv-validation primitives (requiredInProd, httpsUrlInProduction, …).
@scani/deadlineA bound on one await of a remote dependency. No dependencies of its own, so any package may take it.

Outbound network adapters.

@scani/providers (packages/clients/providers)

Section titled “@scani/providers (packages/clients/providers)”

The unified 3rd-party integration package. One directory per provider under src/providers/ (CoinGecko, DeFiLlama, Kraken, Binance, IBKR, Wise, OpenAI, Solana, …). Capability-based interfaces (PriceProvider, BalanceProvider, TransactionProvider, TokenIdentityProvider, AIProvider).

See Adding a provider and Provider matrix.

@scani/cloud-client (packages/clients/cloud-client)

Section titled “@scani/cloud-client (packages/clients/cloud-client)”

Typed tRPC client for the data-provider. The api + worker call the data-provider through this rather than reaching for HTTP directly.

Design system + shared client plumbing for the SPA. Tailwind preset + CSS tokens, the full shadcn primitive set, ThemeContext, ErrorBoundary, UpdateBanner, MagicCodeInput, the useAppUpdate hook, PWA helpers, createScaniAuthClient / createTrpcProvider factories.

apps/frontend/app is the canonical source of truth for shared primitives — when promoting a new primitive into the library, copy from there.

WhatWhere
tRPC routersapps/backend/api/src/presentation/routers/
Queue names + enqueue helperspackages/infra/queue/src/{queue-names,enqueue}.ts
Worker processorsapps/backend/worker/src/processors/
Repeatable schedulespackages/business/jobs/src/scheduled-jobs/
Domain services / repos / use casespackages/business/domain/src/
DB schemapackages/infra/db/src/schema/
Drizzle migrationspackages/infra/db/src/migrations/
Provider registrypackages/clients/providers/src/
Data-provider routersapps/backend/data-provider/src/presentation/
Test preloadpackages/business/domain/test-preload.ts