Client onboarding and provisioning
The intended tenant model is one Supabase project per client application. A Supabase project contains one primary PostgreSQL database, normally named postgres; module separation is implemented with tables inside that database, not multiple PostgreSQL databases inside one project.
For a client using Shopify, create a Shopify Supabase project, store its connection metadata against the client's organization, and provision the standard 13-table schema.
Prerequisites
- The backend organization already exists.
- A
ShopifyConnectionexists and has the permanentmyshopify_domainused in webhook attributes. - The client's Supabase project has been created and its direct PostgreSQL connection details are available.
DB_CREDENTIAL_FERNET_KEYis configured before encrypting or decrypting the database password.- Network policy allows the worker VM to reach the Supabase PostgreSQL endpoint.
Do not commit a Supabase DSN, database password, Fernet key, Shopify secret, or Google service-account JSON. Supply them through the deployment environment or a secret manager.
Step 1: Create the Supabase project
Create one project for the client's Shopify data. Record:
| Required value | Example shape |
|---|---|
| Host | db.<project-ref>.supabase.co |
| Port | 5432 for a direct PostgreSQL connection |
| Database | postgres |
| Username | Project database user |
| Password | Project database password |
| SSL mode | require |
Connection pooling can be used only after validating transaction semantics and credentials for the selected pool mode. The current storage layer opens a normal psycopg connection per event.
Step 2: Register the target in the application database
The normal production path requires one Database and one related DatabaseCredential for the organization. The following Django shell pattern avoids putting the password in source code:
export CLIENT_SHOPIFY_DB_PASSWORD='set-through-a-secret-manager'
python manage.py shell
import os
from database.models import Database
from database.models.database_credential import DatabaseCredential
organization_id = 123
database, _ = Database.objects.update_or_create(
organization_id=organization_id,
database_name="shopify",
defaults={"type": "shopify", "is_deleted": False},
)
credential, _ = DatabaseCredential.objects.get_or_create(database=database)
credential.db_type = "postgresql"
credential.host = "db.PROJECT_REF.supabase.co"
credential.port = 5432
credential.db_name = "postgres"
credential.username = "postgres"
credential.set_password(os.environ["CLIENT_SHOPIFY_DB_PASSWORD"])
credential.save()
The storage resolver first searches Database.type = shopify; using both the type and database name keeps the primary and fallback lookups unambiguous.
Step 3: Provision all Shopify tables
Run the idempotent management command:
python manage.py provision_shopify_storage \
--org-id 123 \
--shop-domain client-store.myshopify.com
The expected success message lists shopify_events and all 12 resource tables. The shop domain is context for logging only; database selection is based on org-id.
The command can be run again safely because table and index creation uses IF NOT EXISTS.
Step 4: Verify the target schema
Connect to the client's Supabase database and verify the expected tables:
SELECT tablename
FROM pg_catalog.pg_tables
WHERE schemaname = 'public'
AND tablename LIKE 'shopify_%'
ORDER BY tablename;
Thirteen rows should be present. Verify the event ledger uniqueness and indexes:
SELECT indexname, indexdef
FROM pg_catalog.pg_indexes
WHERE schemaname = 'public'
AND tablename = 'shopify_events'
ORDER BY indexname;
Step 5: Verify routing without using the global override
Ensure SHOPIFY_SUPABASE_DATABASE_URL is unset on a multi-client worker. Send a controlled event for the client's myshopify_domain, then confirm it appears only in that client's project:
SELECT event_id, topic, resource_type, resource_id, received_at
FROM shopify_events
ORDER BY received_at DESC
LIMIT 10;
For an order event with an ID, verify the snapshot:
SELECT shopify_id, topic, is_deleted, first_seen_at, last_seen_at
FROM shopify_orders
ORDER BY last_seen_at DESC
LIMIT 10;
Connection resolution decision tree
Environment variables
| Variable | Required | Purpose |
|---|---|---|
PROJECT_ID | Worker | Full Google Cloud project ID used to build the subscription path |
SUBSCRIPTION_ID | Worker | Pub/Sub subscription consumed by run_subscriber |
GOOGLE_APPLICATION_CREDENTIALS or ADC | Worker | Google credential source with Pub/Sub Subscriber access |
DB_CREDENTIAL_FERNET_KEY | Per-client credential path | Encrypts and decrypts DatabaseCredential.encrypted_password |
SHOPIFY_SUPABASE_SSLMODE | Optional | PostgreSQL SSL mode; defaults to require |
SHOPIFY_SUPABASE_DATABASE_URL | Optional override | Sends all worker writes to one DSN and bypasses per-org lookup |
SHOPIFY_WEBHOOK_CALLBACK_URL | Registration | External gateway URL registered with Shopify |
Onboarding acceptance checklist
- Organization and permanent Shopify domain resolve to exactly one
ShopifyConnection. - Shopify
Databaserow is non-deleted and owned by the same organization. - Credential password decrypts using the deployed Fernet key.
- Provisioning returns all 13 tables without error.
- A controlled event creates one ledger row and one expected snapshot row.
- A repeated delivery updates rather than duplicates the composite-key rows.
- A delete topic sets
is_deleted = truewithout physically removing the snapshot. - The global Supabase override is absent from multi-client production workers.