[{"data":1,"prerenderedAt":550},["ShallowReactive",2],{"content-query-qnTWlPUlIy":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":7,"summary":9,"order":10,"body":11,"_type":544,"_id":545,"_source":546,"_file":547,"_stem":548,"_extension":549},"\u002Fguides\u002Fwhy-per-tenant-database-files","guides",false,"","Why per-tenant database files — FastYoke","The \"Multi-Tenancy Prime Directive,\" explained for buyers. Why FastYoke isolates customer data at the operating-system layer instead of trusting WHERE tenant_id = ?.",5,{"type":12,"children":13,"toc":523},"root",[14,22,499],{"type":15,"tag":16,"props":17,"children":21},"element","hero",{"eyebrow":18,"tagline":19,"title":20},"Architecture","Every multi-tenant SaaS will tell you tenant data is isolated. The interesting question is what's holding the isolation up. Here's the architectural choice FastYoke made differently, and what it buys you.","Why per-tenant database files",[],{"type":15,"tag":23,"props":24,"children":27},"marketing-section",{"band":25,"max-width":26},"white","3xl",[28,36,67,80,99,105,113,132,158,163,169,176,197,203,223,235,241,246,252,257,262,268,295,301,306,312,331,337,342,348,360,366,371,377,389,444,449,455,474,479],{"type":15,"tag":29,"props":30,"children":32},"h2",{"id":31},"the-default-that-most-multi-tenant-saas-ships",[33],{"type":34,"value":35},"text","The default that most multi-tenant SaaS ships",{"type":15,"tag":37,"props":38,"children":39},"p",{},[40,42,57,59,65],{"type":34,"value":41},"Pick any multi-tenant SaaS product built since 2010 and\nthe data model is almost always the same: ",{"type":15,"tag":43,"props":44,"children":45},"strong",{},[46,48,55],{"type":34,"value":47},"one shared\ndatabase, one or more tables per resource, every row\ntagged with a ",{"type":15,"tag":49,"props":50,"children":52},"code",{"className":51},[],[53],{"type":34,"value":54},"tenant_id",{"type":34,"value":56}," column.",{"type":34,"value":58}," Every query has a\n",{"type":15,"tag":49,"props":60,"children":62},{"className":61},[],[63],{"type":34,"value":64},"WHERE tenant_id = ?",{"type":34,"value":66}," clause. Every endpoint resolves the\ncaller's tenant from the session and threads that value\nthrough.",{"type":15,"tag":37,"props":68,"children":69},{},[70,72,78],{"type":34,"value":71},"The application code is solely responsible for never\nforgetting that clause. If a developer ships a query\nwithout it — or with the wrong tenant id — ",{"type":15,"tag":73,"props":74,"children":75},"em",{},[76],{"type":34,"value":77},"every\ntenant's data is exposed by that one query",{"type":34,"value":79},". The\ndatabase can't help you, because the rows look the same\nto it.",{"type":15,"tag":37,"props":81,"children":82},{},[83,85,90,92,97],{"type":34,"value":84},"This is the world most SaaS lives in. It works\n",{"type":15,"tag":73,"props":86,"children":87},{},[88],{"type":34,"value":89},"mostly",{"type":34,"value":91},". It produces a steady tail of CVEs, postmortems,\nand incident reports that begin \"a query was missing its\ntenant scope.\" The pattern is so common it has a name:\n",{"type":15,"tag":43,"props":93,"children":94},{},[95],{"type":34,"value":96},"broken object-level authorization",{"type":34,"value":98}," — number one on the\nOWASP API top-10 for several years running.",{"type":15,"tag":29,"props":100,"children":102},{"id":101},"what-fastyoke-does-instead",[103],{"type":34,"value":104},"What FastYoke does instead",{"type":15,"tag":37,"props":106,"children":107},{},[108],{"type":15,"tag":43,"props":109,"children":110},{},[111],{"type":34,"value":112},"Every tenant gets its own SQLite database file.",{"type":15,"tag":37,"props":114,"children":115},{},[116,118,123,125,130],{"type":34,"value":117},"The application doesn't have one database connection that\nmultiplexes between tenants. It opens ",{"type":15,"tag":73,"props":119,"children":120},{},[121],{"type":34,"value":122},"the file that\nbelongs to that tenant",{"type":34,"value":124},", runs the query, and closes the\nhandle. There is no ",{"type":15,"tag":49,"props":126,"children":128},{"className":127},[],[129],{"type":34,"value":54},{"type":34,"value":131}," column on the tenant-side\ntables — there's no need, because no other tenant's rows\nlive in the file in the first place.",{"type":15,"tag":37,"props":133,"children":134},{},[135,137,142,144,149,151,156],{"type":34,"value":136},"Cross-tenant access isn't ",{"type":15,"tag":73,"props":138,"children":139},{},[140],{"type":34,"value":141},"guarded against",{"type":34,"value":143},". It's\n",{"type":15,"tag":43,"props":145,"children":146},{},[147],{"type":34,"value":148},"mechanically impossible.",{"type":34,"value":150}," A bug that forgets to scope\na query can't leak anyone else's data, because the\napplication never has two tenants' files open at the same\ntime. The wrong query against the wrong file just\nreturns the wrong tenant's own rows. Hostile inputs that\nwould coax a ",{"type":15,"tag":49,"props":152,"children":154},{"className":153},[],[155],{"type":34,"value":54},{"type":34,"value":157}," parameter astray have nothing to\ncoax — the parameter isn't in the query.",{"type":15,"tag":37,"props":159,"children":160},{},[161],{"type":34,"value":162},"We call this the \"Multi-Tenancy Prime Directive,\" and it shapes\nthe rest of the architecture.",{"type":15,"tag":29,"props":164,"children":166},{"id":165},"what-that-buys-you",[167],{"type":34,"value":168},"What that buys you",{"type":15,"tag":170,"props":171,"children":173},"h3",{"id":172},"architectural-authorization",[174],{"type":34,"value":175},"Architectural authorization",{"type":15,"tag":37,"props":177,"children":178},{},[179,181,187,189,195],{"type":34,"value":180},"Code review for tenant isolation becomes a different kind\nof question. Instead of \"did the developer remember the\n",{"type":15,"tag":49,"props":182,"children":184},{"className":183},[],[185],{"type":34,"value":186},"WHERE",{"type":34,"value":188}," clause,\" reviewers ask \"which file did this code\npath open?\" That question has a single answer per request\n— ",{"type":15,"tag":49,"props":190,"children":192},{"className":191},[],[193],{"type":34,"value":194},"connection_for(tenant_id)",{"type":34,"value":196}," — and it's invariant across\nevery handler. The proof reduces to one helper, not\ndozens of queries.",{"type":15,"tag":170,"props":198,"children":200},{"id":199},"a-real-exit-story",[201],{"type":34,"value":202},"A real exit story",{"type":15,"tag":37,"props":204,"children":205},{},[206,208,213,215,221],{"type":34,"value":207},"Per-tenant files mean ",{"type":15,"tag":43,"props":209,"children":210},{},[211],{"type":34,"value":212},"your data is one file",{"type":34,"value":214},". If you\nwant to leave FastYoke, you don't run a query that fans\nout across a shared schema. You download your file. The\nSQLite format is open, documented, and supported by every\nlanguage. The marketing claim \"you can take your data\nwith you\" becomes a single ",{"type":15,"tag":49,"props":216,"children":218},{"className":217},[],[219],{"type":34,"value":220},"curl",{"type":34,"value":222}," invocation, not a\nsix-month migration project.",{"type":15,"tag":37,"props":224,"children":225},{},[226,228,233],{"type":34,"value":227},"For ISVs building on top of FastYoke this matters even\nmore: your customer's data lives in ",{"type":15,"tag":73,"props":229,"children":230},{},[231],{"type":34,"value":232},"their",{"type":34,"value":234}," file, and\nthey can demand a copy any time without us being involved.",{"type":15,"tag":170,"props":236,"children":238},{"id":237},"backup-and-forensics-simplicity",[239],{"type":34,"value":240},"Backup and forensics simplicity",{"type":15,"tag":37,"props":242,"children":243},{},[244],{"type":34,"value":245},"A backup is the file. A point-in-time copy is the file at\na moment. A forensic investigation is the file plus the\nevent log inside it. The story is the same shape no\nmatter which tenant is asking.",{"type":15,"tag":170,"props":247,"children":249},{"id":248},"compliance-reviews",[250],{"type":34,"value":251},"Compliance reviews",{"type":15,"tag":37,"props":253,"children":254},{},[255],{"type":34,"value":256},"Buyers in regulated workloads (healthcare, financial\nservices, government) almost always ask \"where exactly\ndoes my data live, and what stops the developer who\nshipped a bug yesterday from leaking it tomorrow?\" The\nanswer that satisfies them is rarely \"we have a code\nreview process.\" The answer that does satisfy them is\n\"the application never has another tenant's file open at\nthe same time as yours.\"",{"type":15,"tag":37,"props":258,"children":259},{},[260],{"type":34,"value":261},"Per-tenant files turn this from a process answer into an\narchitectural answer.",{"type":15,"tag":170,"props":263,"children":265},{"id":264},"region-pinning-free",[266],{"type":34,"value":267},"Region pinning, free",{"type":15,"tag":37,"props":269,"children":270},{},[271,273,279,281,286,288,293],{"type":34,"value":272},"Need a tenant to live in ",{"type":15,"tag":49,"props":274,"children":276},{"className":275},[],[277],{"type":34,"value":278},"us-east-1",{"type":34,"value":280}," for a residency\nrequirement? Move the file to ",{"type":15,"tag":49,"props":282,"children":284},{"className":283},[],[285],{"type":34,"value":278},{"type":34,"value":287},". The boundary\nof ",{"type":15,"tag":73,"props":289,"children":290},{},[291],{"type":34,"value":292},"what is the tenant's data",{"type":34,"value":294}," is the file. No shared\ntable to split.",{"type":15,"tag":29,"props":296,"children":298},{"id":297},"what-that-costs",[299],{"type":34,"value":300},"What that costs",{"type":15,"tag":37,"props":302,"children":303},{},[304],{"type":34,"value":305},"This is real engineering, not a free win.",{"type":15,"tag":170,"props":307,"children":309},{"id":308},"you-cant-run-a-cross-tenant-aggregate-query",[310],{"type":34,"value":311},"You can't run a cross-tenant aggregate query",{"type":15,"tag":37,"props":313,"children":314},{},[315,317,322,324,329],{"type":34,"value":316},"Want \"total submissions across all tenants today\"? In a\nshared-schema model that's one query. In FastYoke, it's a\nfan-out across every tenant file plus a roll-up. We\nmaintain a separate ",{"type":15,"tag":43,"props":318,"children":319},{},[320],{"type":34,"value":321},"platform",{"type":34,"value":323}," database for the\naggregate queries that have to span tenants — billing\ncounters, marketplace metadata, super-admin views. The\nrule we hold is that ",{"type":15,"tag":73,"props":325,"children":326},{},[327],{"type":34,"value":328},"no tenant data",{"type":34,"value":330}," lives in the\nplatform database. Aggregate counts do.",{"type":15,"tag":170,"props":332,"children":334},{"id":333},"file-handle-and-per-tenant-resource-pressure",[335],{"type":34,"value":336},"File-handle and per-tenant resource pressure",{"type":15,"tag":37,"props":338,"children":339},{},[340],{"type":34,"value":341},"Thousands of tenants means thousands of files. SQLite is\nlightweight enough that this works, but the application\nhas to be careful about connection pooling, file\ndescriptor limits, and per-tenant write contention. We\nsize for this; you don't have to think about it.",{"type":15,"tag":170,"props":343,"children":345},{"id":344},"cross-tenant-joins-simply-arent-available",[346],{"type":34,"value":347},"Cross-tenant joins simply aren't available",{"type":15,"tag":37,"props":349,"children":350},{},[351,353,358],{"type":34,"value":352},"Some product shapes — \"show me all customers across all\nmy white-labeled brands\" — don't fit. The platform's\nhonest answer is \"FastYoke is the wrong substrate for\nthat.\" If your product ",{"type":15,"tag":73,"props":354,"children":355},{},[356],{"type":34,"value":357},"is",{"type":34,"value":359}," multi-account-as-one-view,\nshared-schema is the right pattern. We pick the workloads\nwhere the isolation guarantee is worth more than the\nflexibility lost.",{"type":15,"tag":170,"props":361,"children":363},{"id":362},"litestream-litefs-operational-shape",[364],{"type":34,"value":365},"Litestream \u002F LiteFS operational shape",{"type":15,"tag":37,"props":367,"children":368},{},[369],{"type":34,"value":370},"Replicating one file per tenant has a different\noperational story than replicating one shared database.\nWe've built around it (Litestream + LiteFS, with a\nwedged-primary watchdog\nfor the failure mode where the primary stops accepting\nwrites). It works, but it's a more bespoke story than\n\"managed Postgres with a read replica.\"",{"type":15,"tag":29,"props":372,"children":374},{"id":373},"where-this-fits-in-the-bigger-picture",[375],{"type":34,"value":376},"Where this fits in the bigger picture",{"type":15,"tag":37,"props":378,"children":379},{},[380,382,387],{"type":34,"value":381},"Per-tenant files are ",{"type":15,"tag":73,"props":383,"children":384},{},[385],{"type":34,"value":386},"one",{"type":34,"value":388}," piece of FastYoke's isolation\nposture, not the whole thing:",{"type":15,"tag":390,"props":391,"children":392},"ul",{},[393,404,414,424,434],{"type":15,"tag":394,"props":395,"children":396},"li",{},[397,402],{"type":15,"tag":43,"props":398,"children":399},{},[400],{"type":34,"value":401},"OS-layer isolation",{"type":34,"value":403}," — per-tenant files (this guide).",{"type":15,"tag":394,"props":405,"children":406},{},[407,412],{"type":15,"tag":43,"props":408,"children":409},{},[410],{"type":34,"value":411},"Application-layer enforcement",{"type":34,"value":413}," — RBAC catalog, PAT\nscoping with hard refusals, Strategic Partner consent.",{"type":15,"tag":394,"props":415,"children":416},{},[417,422],{"type":15,"tag":43,"props":418,"children":419},{},[420],{"type":34,"value":421},"Encryption boundary",{"type":34,"value":423}," — per-tenant AES-256-GCM keys\nfor tagged PII \u002F SPI fields.",{"type":15,"tag":394,"props":425,"children":426},{},[427,432],{"type":15,"tag":43,"props":428,"children":429},{},[430],{"type":34,"value":431},"Audit boundary",{"type":34,"value":433}," — append-only event ledger per\ntenant.",{"type":15,"tag":394,"props":435,"children":436},{},[437,442],{"type":15,"tag":43,"props":438,"children":439},{},[440],{"type":34,"value":441},"Network boundary",{"type":34,"value":443}," — TLS in transit, region pinning\nfor residency.",{"type":15,"tag":37,"props":445,"children":446},{},[447],{"type":34,"value":448},"Per-tenant files are the load-bearing one. Everything\nelse is a layer of belt-and-suspenders on top of an\narchitecture that already makes the bad outcome\nmechanically impossible.",{"type":15,"tag":29,"props":450,"children":452},{"id":451},"the-honest-summary",[453],{"type":34,"value":454},"The honest summary",{"type":15,"tag":37,"props":456,"children":457},{},[458,460,465,467,472],{"type":34,"value":459},"FastYoke chose per-tenant files because we believe most\nmulti-tenant SaaS leaks are not ",{"type":15,"tag":73,"props":461,"children":462},{},[463],{"type":34,"value":464},"failures of process",{"type":34,"value":466}," —\nthey're ",{"type":15,"tag":73,"props":468,"children":469},{},[470],{"type":34,"value":471},"consequences of the substrate",{"type":34,"value":473},". A substrate\nwhere the right query and the wrong query look identical\nto the database will produce wrong queries no matter how\ngood your code review is. We removed the substrate.",{"type":15,"tag":37,"props":475,"children":476},{},[477],{"type":34,"value":478},"This costs us cross-tenant flexibility, which is real.\nFor the workloads we're built for — operations-heavy\nSMB, ISVs shipping to enterprise, regulated workflows\nwhere the buyer cares more about boundary than about\nquery convenience — that's the right trade.",{"type":15,"tag":37,"props":480,"children":481},{},[482,484,490,492,497],{"type":34,"value":483},"If you're evaluating FastYoke for one of those shapes,\nthis is the architectural commitment underneath every\nother claim on ",{"type":15,"tag":49,"props":485,"children":487},{"className":486},[],[488],{"type":34,"value":489},"\u002Fsecurity",{"type":34,"value":491},". The rest of the security\nposture is real, but it's defense-in-depth around a\nboundary that's ",{"type":15,"tag":73,"props":493,"children":494},{},[495],{"type":34,"value":496},"already",{"type":34,"value":498}," enforced by the operating\nsystem.",{"type":15,"tag":23,"props":500,"children":502},{"band":501,"max-width":26},"gray",[503],{"type":15,"tag":37,"props":504,"children":505},{},[506,508,513,515,521],{"type":34,"value":507},"For the procurement-grade detail — encryption,\nattestations, subprocessor list — see\n",{"type":15,"tag":509,"props":510,"children":511},"a",{"href":489},[512],{"type":34,"value":489},{"type":34,"value":514},". For an architectural conversation\nwith someone who'd be the engineer behind your tenant,\nrequest ",{"type":15,"tag":509,"props":516,"children":518},{"href":517},"\u002Fgetting-started",[519],{"type":34,"value":520},"early access",{"type":34,"value":522},".",{"title":7,"searchDepth":524,"depth":524,"links":525},2,[526,527,528,536,542,543],{"id":31,"depth":524,"text":35},{"id":101,"depth":524,"text":104},{"id":165,"depth":524,"text":168,"children":529},[530,532,533,534,535],{"id":172,"depth":531,"text":175},3,{"id":199,"depth":531,"text":202},{"id":237,"depth":531,"text":240},{"id":248,"depth":531,"text":251},{"id":264,"depth":531,"text":267},{"id":297,"depth":524,"text":300,"children":537},[538,539,540,541],{"id":308,"depth":531,"text":311},{"id":333,"depth":531,"text":336},{"id":344,"depth":531,"text":347},{"id":362,"depth":531,"text":365},{"id":373,"depth":524,"text":376},{"id":451,"depth":524,"text":454},"markdown","content:guides:why-per-tenant-database-files.md","content","guides\u002Fwhy-per-tenant-database-files.md","guides\u002Fwhy-per-tenant-database-files","md",1783575469972]