Field Notes · 7 MIN
One backend, many paid apps: users, payments and runtime config
How one Django backend runs several paid consumer apps: separate users per app, one payment account whose webhooks ignore other apps' orders, and prices changed without a deploy.
Yes, several paid consumer apps can share one backend, as long as each app keeps its own users, every payment is tagged with the app it belongs to, and prices and switches live in runtime config instead of code. We run our own paid products this way on one Django backend. This post follows one backend, many products and focuses on accounts and money.
- One Django project can host many products as separate apps, sharing deploys, admin and monitoring.
- Each app gets its own user table and tokens, because a sign-up for one product is not a sign-up for another.
- One payment account can serve every app if each order carries a note naming the app that created it.
- Confirmation and webhook paths should share one idempotent function keyed on the payment ID.
- Prices and switches live in runtime config, validated so a bad value refuses the order instead of charging it.
- Nactore is a technology and growth company that builds, ships, ranks and grows products on foundations like this, measured on your growth.
What does one backend for many paid apps look like?
One backend for many paid apps is a single Django project where each product is its own app, with its own models, endpoints and admin pages, deployed together. Ours hosts a restaurant menu product, licences for two browser extensions, an email newsletter and pay-first consumer apps. They share the deploy, database and payment account, nothing else.
New products arrive as new apps, not new services. That keeps one place to deploy, one admin to operate and one set of logs to read, which matters when a small team runs several products. The backend's README calls this a modular monolith, and the name fits.
Why should every app have its own users?
Every app should have its own user table because a person who signs up for one product has not signed up for another. Shared accounts create surprises, such as an "account already exists" error from an app the person never used, and they blur each product's leads and marketing lists. Separate pools keep users, leads and sessions clean.
In practice each app defines its own user model and its own opaque bearer tokens, stored as SHA-256 hashes, instead of reusing the framework's default accounts. The extension licences go further and have no account at all, only a licence key.
One gotcha is easy to miss. The project's global default authentication is JWT, so a public endpoint that receives an opaque product token gets a 401 before the view even runs. Public views now switch global authentication off explicitly and check their own token.
How can one payment account serve several apps?
One payment account can serve several apps if every order carries a note naming the app that created it. Each app's webhook verifies the signature, then ignores any order without its own note, so a payment in one product can never grant access in another. Confirmation and webhook share one idempotent function, so whichever arrives first grants access.
| Concern | How we handle it |
|---|---|
| Which app owns a payment | A product note written when the order is created |
| Webhook trust | Signature check on the raw request body |
| Duplicate or replayed events | Unique payment ID and one shared grant function |
| Buyer claims the wrong product | Server re-reads the order and compares its notes |
| Refund or chargeback | Revoke access, never delete the record |
| Account deletion | Payment record kept, its link to the user set to null |
Our first version used a single webhook URL for two products and fell back to the second product when the first had no matching order. It worked, but explicit notes and one endpoint per app are easier to reason about. Razorpay's webhook guide covers the signature check. We call the payment and email APIs with plain standard-library HTTP after a vendor SDK broke on a packaging change. For the rest of the checkout path, see what breaks when you ship a product with payments.
What belongs in runtime config?
Runtime config holds anything a non-engineer should change without a deploy: prices, product names, feature switches and limits. We use django-constance, which stores values in the database and edits them in the admin. Prices are validated, so an out-of-range price makes checkout refuse the order instead of charging the wrong amount.
The django-constance documentation explains the database backend. One behavior is worth knowing before you rely on it. Once a value has been saved, changing the default in code does not change the live value. We learned that when changing a price default in code had to be followed by a data migration before the live price moved. Correct a live value in the admin or with a one-off data migration, and write down which you did.
Secrets never go in runtime config. Keys and webhook secrets stay in the host's environment variables, and production refuses to boot without them.
How do you stop one app from breaking the others?
One app breaks the others when it shares something it should not: authentication, cross-origin rules, a slow dependency or a database quota. We scope each app's cross-origin rules to its own API path, put paid AI calls behind timeouts with a refund on failure, and watch every shared quota, because an exhausted free tier once took every product down together.
- Scoped cross-origin rules. Browser-extension origins may call the licence endpoints only. Adding them globally would let any installed extension call every other product's API.
- Pay on success. A pay-per-use app spends a credit when the result is shown and refunds it if the model call fails.
- Separate invoice series. Each product numbers its own invoices, so the books stay readable per product.
- Shared limits, honestly counted. Our rate limits use per-worker caches, so the real limit is roughly the setting times the number of workers. We note that next to the setting.
For model calls, the fields we log on every request are in what to log for LLM observability.
What should you set up on day one?
On day one, set up the parts that are painful to retrofit: separate users per app, a product note on every order, one idempotent grant function, validated runtime prices, scoped cross-origin rules and per-product invoice numbers. Each one takes minutes with one product and days to add once real payments and users exist.
- A user model per app, with hashed tokens and its own leads table.
- A product note on every order, even while there is only one product.
- One grant function shared by the confirmation path and the webhook.
- Prices in runtime config, with a valid range enforced at checkout.
- Cross-origin rules per API path, never global wildcards.
- A health check that does not depend on any outside provider, so one slow vendor does not mark the whole backend down.
Give every order a product note from the first day, even with one product. The day you add a second app, every old webhook event is already unambiguous.
Frequently asked questions
Is a modular monolith better than microservices for several products?
For a small team running several products, it usually is. One deploy, one admin and one log stream beat a service per product until a product's scale or compliance needs force it out.
Does a person need separate logins for each app?
Yes, in our setup. That is deliberate. Each product owns its users and leads, so nobody meets an account they did not create.
What happens if the payment provider sends a webhook twice?
Nothing new. The payment ID is unique, and the shared grant function returns the existing result instead of granting access or sending an email again.
Can you set up a backend like this for our products?
Yes. See the kind of systems we build on our work page, then tell us what you plan to sell.
Share the plumbing, not the customers
A shared backend pays off when products share infrastructure and keep their customers, payments and limits separate. Get the order notes, idempotency and runtime prices right early, and each new product becomes a new app instead of a new project. Nactore is a technology and growth company, one partner that builds, ships, ranks and grows, measured on your growth.
Running more than one product, or planning to? Book a free 30-minute call and we will look at your setup with you.
Want to apply this to your business?
Book a free 30-minute call. We will tell you what we would do first.