Deploy anywhere
Choose your topology
Start on Supabase with zero extra configuration, then move each area with usertasks-admin switch: a decision guide to Supabase only, hybrid, single cloud (AWS, Azure, Google Cloud), multi-cloud and on-premises, with the rules for what can mix.
UserTasks splits its infrastructure into areas: database, auth, file storage, realtime, scheduler, email, secrets, KMS and cache, plus log export and directory sync, which are set per workspace. Each area has its own provider. A topology is the set of choices you make, and where each chosen service runs.
Every install starts in the same place: on Supabase, with nothing else to configure. Later you can move one area at a time with usertasks-admin switch. Each switch checks the target first, takes a backup, logs every step and can be rolled back. You never have to choose your final topology on day one.
| Topology | What runs where | Pick it when | Guide |
|---|---|---|---|
| Supabase only | One Supabase project, plus Resend for email | You want to be live today with the least to operate | Supabase only |
| Hybrid | Supabase for the database (and usually auth), with some areas on AWS, Google Cloud or Azure | You want files, email, secrets or sign-in in your own cloud account, and Supabase is fine for the rest | Hybrid |
| Single cloud | Every area on AWS, Azure or Google Cloud | One cloud is mandated: a landing zone, one IAM model, committed spend | AWS · Azure · Google Cloud |
| Multi-cloud | Areas spread over two or more clouds and SaaS vendors | Services you must use already live in different places | Multi-cloud |
| On-premises / air-gapped | Everything inside your network, no outbound internet | Regulation, a disconnected network, or full ownership | On-premises |
The engine is the same in every topology: the same fast execution, the same API, MCP server and CLI, and the same row-level security. Only the services around it change.
Start on Supabase, decouple when you're ready
- Deploy on Supabase. Create a project, apply the migrations, set the Supabase keys,
CRON_SECRETandENCRYPTION_KEY, and run the app (see the self-host quickstart). No provider variable is needed: every area uses its default. - Look at what you have.
usertasks-admin statusshows the provider for each area and where that choice comes from.usertasks-admin doctorchecks every area. - Decouple one area at a time. Run
usertasks-admin switch <area> --to <provider> --dry-run, read the plan, then run it again with--yes. The app keeps running between switches, so you can check each one on its own before you move on. - Stop wherever you like. A hybrid install is a finished topology, not a stage on the way to something else.
usertasks-admin status
usertasks-admin switch storage --to s3 --set region=eu-west-1 \
--set bucket_map='{"task-files":"acme-ut-files","logos":"acme-ut-public/logos","brand-fonts":"acme-ut-public/fonts"}' --dry-run
usertasks-admin switch storage --to s3 --set region=eu-west-1 --set bucket_map='…' --yes
usertasks-admin historyThe full reference is Switching providers.
Decision guide
Answer these in order. The first "yes" is your starting recommendation.
- Must it run without internet access, or entirely in your own data centre? → On-premises / air-gapped.
- Does policy require one cloud for everything (a landing zone, one identity and audit model, committed spend)? → AWS, Azure or Google Cloud.
- Do services you must use already live in different places (Okta for identity, storage in another cloud's data lake, a SaaS email or log platform), and you also need the database in your own cloud? → Multi-cloud.
- Is Supabase fine for the database, but you need one or more areas under your own account (files in your bucket, email from your domain through SES or Azure, secrets in your vault, corporate SSO)? → Hybrid.
- None of the above? → Supabase only. It is the least to run, and you can still move any area later.
| If you need… | Do this | Topology |
|---|---|---|
| Files in your own bucket, region or retention policy | switch storage --to s3|azure_blob|gcs | Hybrid |
| Email from your domain through your provider | switch email --to ses|azure_communication|microsoft_graph|gmail|smtp | Hybrid |
| Corporate sign-in (Entra ID, Okta, Google, Keycloak, AD FS) | switch auth --to builtin|oidc|saml and AUTH_SSO_PROVIDERS | Hybrid |
| Credentials in your secret store | switch secrets --to aws_secrets_manager|azure_key_vault|gcp_secret_manager|hashicorp_vault | Hybrid |
| Credentials encrypted with a key held in your KMS | switch kms --to aws_kms|azure_key_vault|gcp_kms|vault_transit | Hybrid |
| More than one app replica | switch cache --to postgres|redis | Any |
| The database in your own account or network | Move auth, realtime and scheduler first, then switch database --to postgres | Single cloud, multi-cloud, on-premises |
| No outbound internet at all | Every area on a self-hosted service | On-premises |
Comparing the topologies
| Supabase only | Hybrid | Single cloud | Multi-cloud | On-premises | |
|---|---|---|---|---|---|
| Setup | Minutes | One switch per area | Infrastructure as code, then the switches | The most planning | Images, network, certificates |
| What you operate | The app | The app and the services you moved | The app, plus managed services in one cloud | Services in several places | Everything |
| Vendors in the data path | Supabase, Resend | Supabase plus one cloud | One cloud | Several | None |
| Identity for service access | Keys in the environment | Keys or the cloud's IAM, per area | One IAM model (managed identity, task role, service account) | Mixed: IAM at home, keys elsewhere | Your own (Vault, mounted files) |
| Latency | App ↔ Supabase region | As Supabase only, plus each moved area | Everything in one region | Each cross-cloud hop is internet latency | Your LAN |
| Cost drivers | Supabase plan, email volume | Plus storage, email and secret API calls | Database, container hosting, egress | Plus cross-cloud egress | Hardware and people |
| Compliance story | Supabase's certifications and region | Sensitive data in your account | One provider's controls and audit logs | Per area, harder to audit | Entirely yours |
None of these is faster at running workflows than another. The engine does the same work in each, and what you will notice is the network distance between the app and its database. Keep those two together.
What can and can't mix
Most areas are independent: any storage works with any database, and any email provider works with any auth provider. The rules below are the exceptions. usertasks-admin switch checks them in its pre-flight step and refuses a switch that would break one.
- The database moves last. Moving the database off Supabase requires auth (
builtin,oidcorsaml), realtime (sse) and the scheduler (workerorexternal) to be off Supabase first. Storage may stay on Supabase Storage for as long as the project exists. - Built-in auth with the Supabase database needs
SUPABASE_JWT_SECRET(the project's JWT secret), so that PostgREST and Supabase Realtime accept the app's sessions, andDATABASE_URLfor theapp_authstore. - SSE realtime works with any database, Supabase included. It needs a session-mode connection for
LISTEN: a direct connection or Supabase's session pooler on port 5432. Transaction-mode poolers (PgBouncer in transaction mode, Supabase's port 6543) don't deliver notifications. - pg_cron needs pg_cron, pg_net and Supabase Vault, which in practice means a Supabase database. On any other PostgreSQL, use the
workeror anexternaltick. - The built-in worker needs a long-running process. It doesn't work on Vercel or other serverless hosts, or on hosts that pause CPU between requests (App Runner, Cloud Run without "CPU always allocated"). Use an external tick there.
- Storage off Supabase stores
storage://references for logos, theme assets and icons, and file values gain aref. The app resolves them at render time and serves files from/files. Moving back to Supabase Storage is aswitch storagetoo, which rewrites them again; old Supabase public URLs keep resolving through/filesafter a switch. supabase_vaultsecrets need the Supabase database. Move secrets to another store before moving the database.- A KMS can't protect its own credentials. The options needed to reach the KMS stay encrypted with
ENCRYPTION_KEY, so keep them in the environment, in mounted files or in an external secret store. A KMS option can't be asecret://reference to thedbsecret store. - More than one replica needs a shared cache. With
memory, every instance counts rate limits on its own and sees revoked credentials only after its cache expires. Usepostgres(no extra service) orredis. - Sign-in emails follow auth. With Supabase Auth, confirmation and password-reset emails come from Supabase's own mailer. With built-in auth they go through your email provider.
- Sessions don't survive an auth switch. Everyone signs in again. Passwords and user ids carry over.
Data residency
- Keep every area in the same jurisdiction. The database holds tasks, data tables and run history, the event stream (72 hours by default) and the audit log. Storage holds uploaded and generated files. Email providers and log destinations receive copies of some data, so pick their regions too (SES and ACS regions, Datadog sites, Azure Monitor and Cloud Logging locations).
- Secrets and keys: Secret Manager has regional endpoints (
GCP_SECRET_MANAGER_ENDPOINT); AWS Secrets Manager, AWS KMS, Key Vault and Cloud KMS are regional resources. Create them in the database's region. - Sovereign and private clouds: Azure Blob, Graph and Key Vault accept endpoint overrides for national clouds and private endpoints. S3 endpoints can point at a VPC endpoint or an on-premises store.
- Backups:
usertasks-admin switchwrites backups toBACKUP_DIR(default./backups/switches) on the machine where it runs. Keep that directory in the same jurisdiction too. - Supabase only and hybrid: your data's location is the Supabase project's region. Choose it when you create the project, because moving a Supabase project to another region means a database switch.
Failure domains
Each service your topology depends on is a separate thing that can fail.
| Area down | What stops | What keeps working |
|---|---|---|
| Database | Everything | Nothing. Put it close to the app and make it highly available |
| Auth | New sign-ins | Existing sessions and API keys (built-in auth keeps sessions in the database) |
| Storage | File uploads and downloads, logos | Workflows that don't touch files |
| Realtime | Live updates | Pages still load and refresh; SSE falls back to polling without its connection |
| Scheduler | Schedules, queued runs, SLAs and log shipping wait | Synchronous runs, tasks, the API. Each job catches up on the next tick |
| Notifications and invitations (reported as not sent) | Everything else | |
| Secrets store | Providers whose credentials can't be resolved once their cached value expires (5 minutes by default) | Areas that don't use secret:// references |
| KMS | Start-up with a non-env KMS (it refuses to start) | Running instances, which keep their unwrapped data keys in memory |
Cache (redis) | Nothing: rate limits fall back to counting per instance | Everything |
The fewer vendors you have, the fewer of these can fail independently. A single-cloud or on-premises topology has one failure domain to design for. A multi-cloud topology has one per cloud, plus the network between them.
Next
- Choosing each service: the options in each area, with on-premises, AWS, Azure and Google Cloud support.
- Infrastructure and providers: every setting.
- Deployment profiles: tested combinations you can run locally, and the Bicep and Terraform templates.
- Optional drivers: which cloud SDKs and database drivers your image includes, and how to add the ones it doesn't.