Assessment, Effort Estimates, and Recommendation
This article provides an honest assessment of both approaches, effort estimates for the re-architecture, and a recommendation.
Architecture Comparison at a Glance
flowchart TD
subgraph risk["Risk Comparison"]
direction TB
R1["Per-app model<br/>Bug affects 1 group"]
R2["Single instance<br/>Bug affects ALL 500 groups"]
R1 -.-|"vs"| R2
end
subgraph isolation["Data Isolation"]
direction TB
I1["Per-app model<br/>Physical isolation<br/>Separate databases"]
I2["Single instance<br/>Logical isolation<br/>useDb switching"]
I1 -.-|"vs"| I2
end
subgraph cost["Cost at 500 Groups"]
direction TB
C1["Per-app model<br/>No Fly.io charges<br/>600/month MongoDB"]
C2["Single instance<br/>50-80/month Fly.io<br/>600/month MongoDB"]
C1 -.-|"vs"| C2
end
style risk fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143
style isolation fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143
style cost fill:#E8F5EE,stroke:#9BC8AB,stroke-width:2px,rx:12,ry:12,color:#404143
Arguments for Single-Instance Multi-Tenant
- Ramblers Head Office preference: reduces their perceived complexity for national rollout
- Operational simplicity at scale: managing 500 Fly.io apps is genuinely harder than managing 1–4
- Transparent billing: a single app with clear, predictable costs avoids any ambiguity around free-tier usage at scale. A large organisation like Ramblers would likely prefer a clean billing relationship with infrastructure providers
- Easier onboarding: adding a new group = database seed + DNS record, not full Fly.io app provisioning
- Central visibility: single log stream, single metrics dashboard, single deployment
Arguments for Keeping the Current Model
- Data isolation is inherent: no risk of cross-group data leakage — it's physically impossible today. Multi-tenancy introduces the risk of a missing
groupCodefilter or a staleuseDb()call. At 500 groups this risk is amplified. - Blast radius: a bug or outage takes down ALL 500 groups simultaneously. A single point of failure at national scale is a serious operational risk.
- Resource isolation: one group's heavy traffic doesn't starve others
- Zero Fly.io compute cost today: each app falls under the $5/month free allowance. However, this advantage may not scale to 500 apps — Fly.io could reasonably view 500 free-tier apps operated by a single organisation as an unintended use of their pricing model.
- The current model already works: single Docker image, automated deployment, proven at scale
- Security posture: separate databases and auth secrets per group is stronger. A multi-tenant data leak across 500 groups could be catastrophic
- Custom domains already work: users are completely unaware of the shared codebase
Recommendation
The current architecture can scale to 500 groups with investment in deployment automation (which is already largely in place). The per-group deployment model provides stronger isolation, lower blast radius, and simpler application code.
If Ramblers Head Office insists on a single instance, Option B (database-per-group with useDb() switching) is the lowest-risk path because it preserves the current data isolation model and minimises code changes.
The key distinction to communicate to Ramblers Head Office:
| What they want | Current status |
|---|---|
| Single codebase | Already true — one repo, one team |
| Single build artifact | Already true — one Docker image |
| Automated group provisioning | Already true — CLI tool creates new groups |
| Single running instance | Not currently, but technically feasible |
The question is whether "single running instance" is a hard requirement or whether "single codebase with automated provisioning" meets their actual need.
Estimated Effort
Estimates are given for two delivery modes: AI-assisted (developer prompting Claude Code or similar) and manual (developer working unassisted).
| Component | AI-assisted | Manual | Risk |
|---|---|---|---|
| Tenant resolution middleware | 2–4 hours | 2–3 days | Medium |
Database switching (useDb()) |
4–8 hours | 3–5 days | High — needs load testing |
| Auth consolidation + group-scoped tokens | 1–2 hours | 1–2 days | Medium |
| S3 consolidation to single bucket | 3–6 hours | 2–3 days | Medium |
| Background job scheduler (group-aware) | 4–8 hours | 3–5 days | Medium |
API route audit (req.db everywhere) |
2–4 hours | 3–5 days | High — AI excels here |
| Super-admin and provisioning UI | 1–2 days | 5–8 days | Medium |
| Migration tooling (13 sites → single) | 3–6 hours | 2–3 days | Medium |
| Load testing at 500-group concurrency | 1–2 days | 3–5 days | High |
| Total | ~5–8 days | ~5–7 weeks |
Why the AI-Assisted Speedup?
The speedup is most dramatic for:
- Route auditing: AI can systematically scan every Express route handler and flag those not using
req.db— tedious for humans, trivial for AI - Middleware and boilerplate: tenant resolution, auth changes, and S3 path rewriting are well-understood patterns
- Migration scripts: database-to-database data movement is mechanical and well-suited to AI generation
The speedup is smallest for:
- Load testing: test execution and monitoring takes real time regardless
- Super-admin UI: involves UX decisions and iterative refinement
- Integration testing: verifying behaviour across 500 groups requires real infrastructure
Next Steps
- Present current architecture to Ramblers Head Office, emphasising single codebase + single artifact + automated provisioning
- Clarify whether "single instance" is a hard requirement or whether the actual need is simpler onboarding/management
- If required, prototype tenant resolution middleware +
useDb()database switching - Load test
mongoose.useDb()under concurrent multi-group traffic (simulate 500 databases) - Assess MongoDB Atlas pricing at M30+ tier for 500 databases
- Assess Fly.io multi-domain TLS certificate management at 500+ domains