Why the three-tier deployment model
The decision
Section titled “The decision”Scani ships one set of binaries (api, worker, data-provider,
frontend-app) that runs in three shapes:
- Tier 1 — fully self-hosted. Everything on your hardware,
including the
data-provider. - Tier 2 — semi-managed.
api+worker+frontend-appon your side;data-providerprovided by a hosted endpoint. - Tier 3 — fully managed. Someone else runs the whole stack.
Two environment variables switch tiers:
SCANI_CLOUD_URL— where to send object storage, email, OG-metadata and token-search calls.SCANI_CLOUD_API_KEY— the bearer the api + worker present.
The alternative we rejected
Section titled “The alternative we rejected”Per-tier forks of the codebase: a self-host build, a managed-service build, a SaaS build. Each with its own auth model, its own deployment shape, its own feature set.
Why we rejected it
Section titled “Why we rejected it”Forks rot. Three codebases means three places to land every feature, three places to fix every bug, three CI matrices, three chances for security findings to apply to two of three forks. The team is small. The fork tax is unaffordable.
The natural seam already existed. Every third-party call (pricing,
AI, blockchain RPC, email) was already centralised on the
data-provider service for testability and rate-limit isolation.
Once the seam is named, the tier choice is just “which endpoint do
api + worker call?”. No fork required.
User credentials must stay on the user’s side. Exchange API keys,
brokerage tokens, screenshot blobs — these are sensitive. They live
on the api, which the user controls (Tier 1) or which their
operator runs (Tier 2). The data-provider only sees requests that
don’t carry user secrets: an object-storage read or write, a rendered
email, a URL to fetch OG metadata for, a symbol to search. The tier
seam is also the credential boundary — no decrypted integration
credential ever crosses it.
What the three tiers look like
Section titled “What the three tiers look like”What this design unlocks
Section titled “What this design unlocks”- One codebase, three deployment shapes. Every PR ships to all three tiers simultaneously.
- The Tier-1→Tier-2 migration is a config change. Re-point
SCANI_CLOUD_URLat the hosted endpoint; provide the issuedSCANI_CLOUD_API_KEY; restart api + worker. Your data stays on your side. - Provider keys are centralised for the calls that cross the seam. A small operator running 50 users provides one bucket and one mail transport for all of them. The intent was for pricing and AI keys to work the same way; the caution above is why they do not yet.
- Tier 1 keeps every credential on one host. No hosted service is involved at all, and the OSS distribution sends no telemetry. Note this is not an air gap: pricing, chain and AI calls still go out to the upstreams whose keys you set, from your own api and worker.
What the design costs
Section titled “What the design costs”- The
data-provideris a separate service to run. In Tier 1 it is a sidecar on the same compose network; the operational cost is trivial. - Two env vars have to match.
SCANI_CLOUD_API_KEYon api + worker must equalDATA_PROVIDER_API_KEYon the data-provider. In Tier 1 single-tenant, both are seeded with the same value in.env.example.
What this rules out
Section titled “What this rules out”- A “merge the api and the data-provider for Tier 1 only” shortcut. The whole point of the seam is that it doesn’t go away in any tier — that’s what makes the contract testable end-to-end.
- Per-tier feature flags that change product behaviour. The product is the same; only the deployment shape differs.