acme.shop.example.com, and a public schema holds what tenants share. On Sevalla, a wildcard custom domain sends every subdomain to the application, and a job migrates every schema before each deployment.
At a glance
This guide changes the application from Deploy a Django application:Environment variables
The deploy guide lists the other variables.
Before you begin
- A Django project deployed on Sevalla with a PostgreSQL database, as in Deploy Django. Use a database that holds no data you need yet: django-tenants expects to manage every schema from its first migration.
- A domain whose DNS you control, such as
shop.example.com. - The web process on a size larger than H1: custom domains aren’t available on H1. See Scaling.
Install django-tenants
Install django-tenants and updaterequirements.txt:
Add the tenant models
Create an app for the tenants and their domains, and an app with the tenants’ own features:customers/models.py
Configure the project
Instorefront/settings.py, replace INSTALLED_APPS with the apps every tenant shares and the apps each tenant gets in its own schema, and name the tenant models:
storefront/settings.py
TenantMainMiddleware first in MIDDLEWARE:
storefront/settings.py
ROOT_URLCONF with the tenants’ URLs and the main site’s URLs on the public schema:
storefront/settings.py
DATABASES with django-tenants’ PostgreSQL backend and its router:
storefront/settings.py
-
TenantMainMiddlewarecomes before Django’s own middleware: it picks the tenant, and so the schema, from the request’s host. If you added the health check middleware, keep it in front ofTenantMainMiddleware, because the probes’ host matches no tenant. -
django.contrib.authanddjango.contrib.sessionsare both shared here. Keep them together, inSHARED_APPSor inTENANT_APPS: a session points at a user, so both tables must be in the same schema. -
DB_URLcomes from the database’s internal connection, which makes it available during the build too. The settings no longer fall back to SQLite, so for local development, add a local PostgreSQL database to.env, for example:.env
DB_URL, and collectstatic stops with ImproperlyConfigured: Set the DB_URL environment variable. Give it a placeholder next to the placeholder SECRET_KEY. collectstatic loads the settings but doesn’t connect to the database:
Dockerfile
Add the views and URLs
shop/views.py
storefront/urls.py
storefront/urls_public.py
requirements.txt included:
customers gets its first migration. shop has no models yet, so Django reports No changes detected in app 'shop'. Commit the migration: without it, the Migrate job logs Your models in app(s): 'customers' have changes that are not yet reflected in a migration and fails with django.db.utils.ProgrammingError: relation "customers_client" does not exist.
Prepare the application on Sevalla
TheMigrate job from the deploy guide needs no change: django-tenants replaces Django’s migrate command with its migrate_schemas, so python manage.py migrate --noinput migrates the public schema with the shared apps and every tenant’s schema with the tenant apps. Its output on the Logs tab starts each line with the schema, such as [standard:public].
1
Add the wildcard domain
In the sidebar, click Applications, then click the application. Click the Domains tab, then click Add custom domain. Enter
shop.example.com as the Domain name, select Wildcard so that every subdomain such as acme.shop.example.com reaches the application too, and click Add custom domain. Add the DNS records that Sevalla shows at your DNS provider, including the pointing record for *.shop.example.com, and wait until the domain’s status is Active. See Wildcard domains.2
Allow the subdomains
Click the Environment variables tab. Update
ALLOWED_HOSTS to storefront-x7k2m.sevalla.app,.shop.example.com, with your application’s sevalla.app domain. The leading dot matches the domain and every subdomain.3
Deploy
Click Deploy in the undeployed changes bar at the bottom of the page.


Create the tenants
django-tenants serves a request only when its host belongs to a tenant, so create thepublic tenant for the main domain first. When the deployment’s status is Deployed, click the Web terminal tab, and in the shell that opens in the Web process, open a Django shell:
acme creates the acme schema and runs the tenant apps’ migrations in it. Create every later tenant the same way, or from your own sign-up view.
The application’s sevalla.app domain matches no tenant, so it returns 404. To show the public site there too, set SHOW_PUBLIC_IF_NO_TENANT_FOUND = True in storefront/settings.py.
Verify
- Open
https://shop.example.com: the page readsShop platform. - Open
https://acme.shop.example.com: the page readsWelcome to Acme Inc.
Deploy with the API
This sequence prepares the application from Deploy with the API and continues in its shell, withAPI, APP_ID, WEB_ID and APP_HOST set. Push the code changes from Configure the project and Add the views and URLs first. Each step names the CLI command and the Sevalla MCP server tool that do the same.
1
Look up the process size
List process resource types (CLI:
listProcessResourceTypesV3). Save S1:sevalla resources process-types. MCP: get_platform_options.2
Size the web process
Update process (It prints
updateProcessV3) moves the web process to S1, which can have custom domains:s1. CLI: sevalla apps processes update "$WEB_ID" --app-id "$APP_ID" --resource-type "$S1_ID". MCP: scale_process with size_id and apply_now: false.3
Add the wildcard domain
Add application domain (It prints
addApplicationDomainV3) with is_wildcard:VERIFICATION. CLI: sevalla apps domains add "$APP_ID" --name shop.example.com, which has no wildcard option. MCP: add_domain with wildcard: true.4
Read the DNS records
List application domains (
listApplicationDomainsV3) returns the records to add at your DNS provider a few seconds after the domain is added; until then, dns_records is null:verification holds a TXT record for _cf-custom-hostname.shop.example.com, and origin holds the records that point shop.example.com and *.shop.example.com at Sevalla. The status changes from VERIFICATION to POINTING once Sevalla has verified the domain, and to ACTIVE once the domain points at Sevalla. CLI: sevalla apps domains list --app-id "$APP_ID". MCP: get_application.5
Find ALLOWED_HOSTS
List environment variables (CLI:
listEnvironmentVariablesV3) returns each variable’s ID:sevalla apps env-vars list --app-id "$APP_ID". MCP: list_environment_variables.6
Allow the subdomains
Update environment variable (CLI:
updateEnvironmentVariableV3):sevalla apps env-vars update "$HOSTS_ID" --app-id "$APP_ID" --value "$APP_HOST,.shop.example.com". MCP: set_environment_variables, which updates a variable by its key.7
Deploy
Trigger deployment (CLI:
triggerDeploymentV3). With Auto-deploy on, the push already started a deployment; this one queues behind it:sevalla apps deployments trigger "$APP_ID". MCP: trigger_deployment.8
Wait for the deployment
Get deployment (CLI:
getDeploymentV3) reports the status, which ends as success, failed, cancelled or skipped. The loop keeps polling through a failed request, for up to 30 minutes:sevalla apps deployments get "$DEPLOYMENT_ID" --app-id "$APP_ID". MCP: get_deployment.9
Create the tenants
Run a command in a process (It prints
execProcessCommandV3) runs the code from Create the tenants in the web process. Creating a tenant’s schema takes a few seconds, so allow up to 60:0. CLI: sevalla apps processes exec "$WEB_ID" --app-id "$APP_ID" --timeout 60 -- python manage.py shell -v 0 -c "<the same code>". MCP: sevalla_api_update with execProcessCommandV3 and the same body.ACTIVE, run the checks in Verify.