All projects
Featured case study

DUBTEL AI Platform Backend

The multi-tenant FastAPI backend for an AI voice-agent platform, built solo: tenants, role hierarchy, SAML SSO, pricing, recurring invoicing, and CI/CD.

Role
Backend engineer (built solo); led the Next.js portal
System
DUBTEL AI — platform for businesses to run AI voice agents for outreach and support
A tenant hierarchy from System to tenants, clients, and users

Results at a glance

  • A four-level hierarchy (System → Tenant → Business Client → User) with every list and write scoped through one shared helper.
  • SAML 2.0 SSO with just-in-time provisioning. Password and SSO logins return identical sessions.
  • Three-layer pricing (platform default, per-tenant wholesale, tenant resale) with floor validation, and a scheduled job that invoices every active resource on the 1st of the month, with PDF invoices.
  • An audit log written by every meaningful mutation, visible per role.
  • Migrated from AlloyDB to DigitalOcean managed Postgres with verified TLS, and shipped QA and production pipelines on GitLab CI.

Overview

DUBTEL AI is a platform where businesses run AI voice agents for outreach and support calls. It is sold through resellers, so the backend has to answer one question correctly on every request: whose data is this, and who is allowed to see it?

I built that backend alone in FastAPI with async SQLAlchemy, asyncpg, and Alembic. I also led the Next.js portal that runs on top of it.

The model

Four levels of access:

RoleSees
SystemEvery tenant
Tenant AdminTheir whole tenant
Account AdminTheir own business client
Account UserTheir own business client, read-only

Resources hang off that hierarchy: AI agents and their prompts, phone numbers (including port-in requests), sub-accounts, calendar events, bills, and an activity log. Tenants can white-label the portal with their own logo and a custom domain.

One scoping rule

Every resource router scopes its queries through a single scope_conditions() helper. System gets no filter, tenants get their tenant, and account roles get their business client.

That rule earned its place when one router didn't use it. list_business_clients had its own hand-written tenant_code filter that missed the System case. With one tenant in the database it looked fine. With two, System Admin would have silently lost sight of every other tenant's clients. I moved it onto the shared helper and added a seed script that creates a second tenant, so cross-tenant scoping gets exercised on QA instead of assumed.

The same pass found a trust gap. Agent create and edit accepted a business_client_id from the payload without checking it belonged to the caller's tenant. The API now validates it.

Auth

  • Password reset. Tokens are single-use: they're invalidated by a password_changed_at stamp that every password change updates. Reset links are built from the request's Origin when it's a trusted domain, so QA emails stop linking to production.
  • Rate limiting across workers. The first forgot-password limiter lived in a module-level dict. The API runs four uvicorn workers, so two quick requests landing on different workers each saw an empty dict, and nothing was ever throttled. It's now backed by a small database table that all workers share.
  • SAML 2.0 SSO. An SP-initiated flow built on python3-saml. It stays inert until the SAML_* variables are set. The assertion consumer never puts a session token in a redirect. It issues a short-lived exchange code in the URL fragment, which the portal swaps for a session. The first SSO login provisions the user just in time. Password and SSO logins share one build_login_response(), so their sessions are identical.

Pricing and invoicing

Pricing has three layers: the platform default, a per-tenant wholesale override, and the tenant's own resale price, with floor validation so a tenant can't sell below cost. An in-process APScheduler job generates flat-fee invoices for every active phone number, sub-account, and AI agent on the 1st of each month. Invoices render to PDF on the server with WeasyPrint.

Billing summary cards (unpaid balance, overdue count) got their own endpoint. They had been computed from the current page of rows, so they were quietly wrong once billing ran past one page.

Infrastructure

  • Database migration. When the AlloyDB trial ended, I moved the database to a DigitalOcean managed Postgres cluster that requires sslmode=verify-full. asyncpg doesn't understand libpq-style sslmode and sslrootcert parameters, so I build a real ssl.SSLContext and pass it through connect_args. That had to be done in all three places that create an engine: the app, Alembic, and the seed script. I made the seed script reuse the app's engine instead of building its own.
  • CI/CD. GitLab CI deploys the qa branch to QA and master to production (manual trigger), using Docker Compose behind nginx.
  • Audit log. The log table already existed but nothing wrote to it, and any logged-in user could read it. I added a log_event() helper, called it from every meaningful mutation, and put the endpoint behind the role scoping above.

Results

  • A reseller-ready, multi-tenant backend, built and owned end to end by one engineer
  • Isolation between tenants enforced by one helper and tested with real second-tenant data
  • SSO, audit logging, pricing, and automated invoicing in production shape

What I took from it

  • Scoping rules that are copied by hand drift. The bug that would have hidden every other tenant's data only shows up once a second tenant exists, so test with two.
  • Anything kept in process memory is per-worker. A rate limiter in a module-level dict does nothing behind four uvicorn workers.
  • Never put a real session token in a redirect URL. Hand out a short-lived exchange code instead.
  • Filter and search on the server. Filtering one page on the client silently misses matches on the other pages.
Henry Iddirisu

Henry Iddirisu

AI product engineer · Accra, Ghana · Remote

More projects

Next.jsReactTypeScript

DUBTEL AI Portal

Role: Lead frontend engineer

The white-label Next.js portal where businesses manage AI voice agents, phone numbers, billing, and users, with role-aware navigat…