Skip to main content

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 ShopifyConnection exists and has the permanent myshopify_domain used in webhook attributes.
  • The client's Supabase project has been created and its direct PostgreSQL connection details are available.
  • DB_CREDENTIAL_FERNET_KEY is configured before encrypting or decrypting the database password.
  • Network policy allows the worker VM to reach the Supabase PostgreSQL endpoint.
Secret handling

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 valueExample shape
Hostdb.<project-ref>.supabase.co
Port5432 for a direct PostgreSQL connection
Databasepostgres
UsernameProject database user
PasswordProject database password
SSL moderequire

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

VariableRequiredPurpose
PROJECT_IDWorkerFull Google Cloud project ID used to build the subscription path
SUBSCRIPTION_IDWorkerPub/Sub subscription consumed by run_subscriber
GOOGLE_APPLICATION_CREDENTIALS or ADCWorkerGoogle credential source with Pub/Sub Subscriber access
DB_CREDENTIAL_FERNET_KEYPer-client credential pathEncrypts and decrypts DatabaseCredential.encrypted_password
SHOPIFY_SUPABASE_SSLMODEOptionalPostgreSQL SSL mode; defaults to require
SHOPIFY_SUPABASE_DATABASE_URLOptional overrideSends all worker writes to one DSN and bypasses per-org lookup
SHOPIFY_WEBHOOK_CALLBACK_URLRegistrationExternal gateway URL registered with Shopify

Onboarding acceptance checklist

  • Organization and permanent Shopify domain resolve to exactly one ShopifyConnection.
  • Shopify Database row 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 = true without physically removing the snapshot.
  • The global Supabase override is absent from multi-client production workers.