What a Single Shared Database Means for Your Data
17 apps and roughly 41 Workers write to one D1 database, not 17 separate ones. Here's honestly how tenant isolation actually works on top of that — and what it isn't.
Once a technical evaluator learns that all 17 Piskes apps and roughly 41 Workers share one D1 database — 195 tables, one instance — the next question is predictable and fair: how is one customer's data kept separate from another's? Better to answer that plainly than let it surface unaddressed later in a security review.
The honest answer: isolation is enforced at the application layer, not the database layer. Every table that holds tenant data carries an owner or workspace identifier, and every query path is expected to filter on it — but that expectation is enforced by code and by a standing automated check, not by a database-level access-control mechanism like row-level security or by giving each customer a physically separate database.
The concrete mechanism is a guardrail script that runs at commit time and fails the build if a new by-id API route — the kind of route that fetches a record by its ID — doesn't include a tenancy check confirming the requester actually owns that record. That check exists because of real history: nine actual IDOR (insecure direct object reference) bugs were found and fixed across by-id routes before this guardrail existed, and the script converts that one-time cleanup into a standing rule that catches the same class of mistake automatically, before it ships, rather than relying on every future PR review to catch it manually.
Why the distinction matters, and why it's worth stating plainly
A knowledgeable reader evaluating this platform will ask specifically whether isolation is enforced at the database level or the application level, because those are genuinely different security postures with different failure modes. Database-level row-level security or per-tenant databases fail closed by default — a bug in application code still can't cross the boundary, because the database itself refuses the query. Application-enforced isolation fails open by default unless every code path that touches tenant data gets it right — which is exactly why an automated, build-blocking check on every new route matters as much as it does here: it's compensating for the specific weakness inherent in enforcing isolation at the application layer rather than pretending that weakness doesn't exist.
Inflating this into "your data is isolated by the database" when it's actually isolated by application logic plus an automated guardrail would be the kind of claim that erodes trust the moment a careful reader checks it against the actual architecture. The accurate version is more modest and, for exactly that reason, more trustworthy: one shared database, tenant isolation enforced in code, and a build-time guardrail — grown out of nine real bugs it was built to stop from recurring — standing between a new route and the same mistake happening again.