{"slug": "inside-lubot-s-database-per-tenant-architecture", "title": "Inside LuBot's database-per-tenant architecture", "summary": "Solo founder Lubo Bali built LuBot, a plain-English business analytics chat product, on a database-per-tenant architecture that provisions an isolated Neon Postgres branch for each paying customer at Stripe checkout, a setup he said took six days instead of a full quarter of work with traditional Postgres. Supabase remains the control plane for organizations, users, and billing state, while each tenant gets 52 tables across four schemas (files, public, shared, stock) in its own Neon branch, with idle tenants scaling to zero and active computes running a 0.25 CU floor and 4 CU burst ceiling. Bali said a single idle tenant would cost about $20 a month without autoscaling and scale to zero, and that all database access routes through one function, get_tenant_engine, in services/tenant_resolver.py.", "body_md": "**“It took six days to go from zero to isolated Postgres per tenant, provisioned the moment someone pays. A traditional Postgres setup would have been a full quarter of work.”**\n\nLubo Bali, Founder of [LuBot](https://lubot.ai/)\n\n[LuBot](https://lubot.ai) is a chat product for business analytics [built by a solo founder](https://www.linkedin.com/in/lubo-bali/). It allows users to ask questions in plain English about their data while pulling info from their portfolio and stocks, website traffic, and files such as Excel spreadsheets or PDFs.\n\nRight since it first started, LuBot was built as a multi-tenant SaaS:\n\n- Clerk handled identity\n- Stripe handled checkout\n- Admin data lived in Supabase, scoped per organization. This was a natural starting point, but a single shared database was never going to give each customer true isolation.\n\n## Why one shared database was not enough\n\nVery early on, Lubo knew they needed stronger isolation than storing every customer's data in shared tables:\n\n- Each tenant in LuBot has 52 tables across four schemas: `files` ,`public` ,`shared` , and`stock`\n- Those schemas map to the three product modes\n- Files live in `files`\n- Website and shared application data live in `public` and`shared`\n- Portfolio and market data live in `stock`\n\nGiving every customer a separate Postgres database would provide a clear isolation boundary, but building the provisioning, routing, and lifecycle infrastructure for traditional Postgres instances in Supabase would be too much work for one person, and the resulting setup would be too expensive to maintain and to scale.\n\nNeon offered a different model thanks to branching:\n\n- A branch is an isolated, copy-on-write clone of its parent\n- Branches have their own compute resources, scale independently, and changes in one branch do not affect its parent or other branches\n- They can also be deployed through the [Neon API](https://neon.com/docs/guides/branching-neon-api)\n\n## Using Neon to deploy one Postgres database per customer\n\nLubo split responsibilities cleanly:\n\n- Supabase stays as the control plane, holding the admin and account data shared across the product: organizations, users, and billing state\n- Neon holds the tenant data. Each paying customer gets their own isolated Postgres database, created the moment they become a paying user.\n\n**“In LuBot, Supabase holds the admin layer and Neon holds the tenant data. Neon is what allows me to provision a database at checkout with idle tenants scaling to zero, so I am not paying for compute when nobody is querying.”**\n\nLubo Bali, Founder of [LuBot](https://lubot.ai/)\n\nThe flow looks like this:\n\n1. The customer completes checkout through Stripe\n2. A Stripe webhook calls the Neon branches API\n3. LuBot creates a branch with a deterministic name, so webhook retries do not create a duplicate tenant\n4. The application runs its schema migration against the new branch\n5. LuBot stores the branch's connection URL against the user row\n6. The customer's next request is already routing to their own database\n\nThe whole flow sits behind one function, `get_tenant_engine`, in `services/tenant_resolver.py`. Every database operation in LuBot goes through that function, and application code asks for a tenant-specific engine. It does not choose a connection string, create a branch, or decide which schema to hit.\n\n## Scaling active tenants without paying for idle compute\n\n**“Without autoscaling and scale to zero, a single idle tenant would cost about $20 a month. With Neon, idle tenants are practically free, and active computes only burst capacity if they have to. That is what makes this setup possible at scale.”**\n\nLubo Bali, Founder of [LuBot](https://lubot.ai/)\n\nWhat makes this architecture doable in Neon is not only the ability to manage branches via the API but also its serverless compute model.\n\nEach tenant compute can autoscale. In LuBot, the most common configuration is a 0.25 CU active floor and a 4 CU burst ceiling. After 300 seconds without activity, the compute suspends. LuBot stops paying for compute while it is suspended, though storage remains billed. That combination is what makes a database-per-tenant design viable on a bootstrap budget.\n\n## Tips on pooled connections, search_path, and pool leaks\n\nIf you run a database per tenant behind a pooler, a few things are worth knowing before you hit them in production.\n\nLuBot connects through Neon's pooled connection string, which uses PgBouncer in transaction mode. That is the right default for a request-driven FastAPI app: it accepts [up to 10,000 client connections](https://neon.com/docs/connect/connection-pooling) and hands each one back to the pool as soon as a transaction ends.\n\nThe tradeoff is that transaction mode does not keep session state. Anything you set with `SET` is gone once the connection returns to the pool. For a tenant database split across four schemas, the setting that bites is `search_path`:\n\nOn a pooled connection, that `SET` only lasts for the current transaction. The next query can land on a recycled connection with a different path, and tables in `files` or `stock` suddenly look missing even though the branch is healthy.\n\nA few habits will keep this from happening:\n\n- Use the pooled URL for application traffic, and the direct URL for migrations, `pg_dump` , and anything that needs session state\n- Set `search_path` per query or at the role level, not once per session\n- Reset connections on check-in, so one tenant's session state cannot leak into the next request through a reused connection\n- Use deterministic branch names, so a retried Stripe webhook never creates a duplicate tenant\n\n## Give your end users their own isolated database\n\nLubo started the tenant resolver on April 23, 2026. By April 29, Stripe checkout could provision a branch, migrate its schema, save the connection URL, and route the next request. Six days from zero to a working per-user Neon branch on payment.\n\nIf your application needs a database per customer, agent, or environment, start with [branching through the Neon API](https://neon.com/docs/guides/branching-neon-api), or ask your agent to build this.\n\n*Thank you to Lubo Bali for sharing LuBot's architecture. If you would also like to share how you are building with Neon, tell us in the [Neon community on Discord](https://neon.com/discord).*", "url": "https://wpnews.pro/news/inside-lubot-s-database-per-tenant-architecture", "canonical_source": "https://neon.com/blog/inside-lubots-database-per-tenant-architecture", "published_at": "2026-09-14 12:00:00+00:00", "updated_at": "2026-09-28 16:46:46.925225+00:00", "lang": "en", "topics": ["ai-products", "ai-startups", "ai-infrastructure", "mlops", "developer-tools"], "entities": ["LuBot", "Lubo Bali", "Neon", "Supabase", "Stripe", "Clerk", "get_tenant_engine", "services/tenant_resolver.py"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/inside-lubot-s-database-per-tenant-architecture", "markdown": "https://wpnews.pro/news/inside-lubot-s-database-per-tenant-architecture.md", "text": "https://wpnews.pro/news/inside-lubot-s-database-per-tenant-architecture.txt", "jsonld": "https://wpnews.pro/news/inside-lubot-s-database-per-tenant-architecture.jsonld"}}