Cheap Postgres Options
Background¶
Azure Database for PostgreSQL Flexible Server is deployed on dev and prod via
infra/modules/postgres.bicep. The current prod server (dna-prd-portfolio-ne-pg) runs
on Standard_D2s_v3 (GeneralPurpose, ~€44/mo). A planned downgrade to Standard_B1ms
(Burstable, ~€6–12/mo) is blocked by CapacityNotAvailable in northeurope as of
2026-05-03 — deploy_postgres = false is temporarily set in parameters.prod.bicepparam.
This document evaluates all cheap Postgres options and makes a recommendation.
Options Compared¶
Option 1 — Azure Flexible Server Burstable B1ms (~€6–12/mo)¶
The cheapest Azure-native option. Already configured in postgres_sku = 'Standard_B1ms' in
prod params — just blocked by capacity.
| Property | Value |
|---|---|
| Cost | ~$12.41/mo compute + storage (West Europe) |
| vCores / RAM | 1 vCore / 2 GiB |
| Managed Identity auth | ✅ Full support (current setup) |
| Azure-native | ✅ Same region, no cross-cloud latency |
| Availability | ❌ Blocked by northeurope CapacityNotAvailable (transient) |
Action: Retry az postgres flexible-server update --sku-name Standard_B1ms --tier Burstable
periodically. Restore deploy_postgres = true once it succeeds.
Option 2 — Neon Serverless Postgres (Free → $19/mo)¶
Pure SaaS, runs on AWS Frankfurt for EU workloads. No Azure resource involved.
| Property | Value |
|---|---|
| Free tier | 0.5 GB storage, 191 compute-hours/month, scale-to-zero |
| Paid (Launch) | $19/mo — more compute hours, no project pausing |
| Managed Identity auth | ❌ Password/connection string only |
| Cold start | ~1–2s (Neon) + ~2s (ACA) = ~3–4s combined on dev |
| Cross-cloud latency | ~5–20ms (AWS Frankfurt → Azure West Europe) |
| Azure Native Integration | ~~Available~~ — retired 2025, use direct SaaS |
| DB branching | ✅ Instant branches per PR |
Catch: Migrating to Neon requires removing all Managed Identity auth code
(ManagedIdentityCredential, IMDS token fetch, AAD admin registration) and replacing with
a connection string stored in Key Vault. Significant regression in security posture.
Option 3 — Supabase (Free → $25/mo)¶
| Property | Value |
|---|---|
| Free tier | 500 MB DB — pauses after 7 days inactivity |
| Pro plan | $25/mo — always-on, no pausing |
| Managed Identity auth | ❌ Connection string only |
| Extras | Auth, object storage, edge functions bundled |
More expensive than Neon for pure Postgres. Only worth it if bundled features (auth, storage) would replace other services. Not relevant here.
Option 4 — Railway (~$5–10/mo)¶
| Property | Value |
|---|---|
| Cost | Usage-based, ~$5–10/mo for low traffic |
| Free credit | $5/mo |
| Managed Identity auth | ❌ Connection string only |
| Notes | Good DX, less enterprise-grade, no Azure integration |
Cost Summary¶
| Option | Monthly cost | MI auth | Notes |
|---|---|---|---|
| Azure B1ms (Burstable) | ~$12/mo | ✅ | Blocked by northeurope capacity (transient) |
| Neon Launch | $19/mo | ❌ | Cheapest if MI auth removed |
| Neon Free | $0/mo | ❌ | Dev only — scale-to-zero, 191 compute-hours |
| Railway | ~$5–10/mo | ❌ | Usage-based |
| Supabase Pro | $25/mo | ❌ | Overkill |
| Azure D2s_v3 (current) | ~$44/mo | ✅ | Status quo |
Recommendation¶
Keep Azure Flexible Server, downgrade to B1ms when northeurope capacity recovers.
The existing Managed Identity auth stack (AAD-only, no secrets, ManagedIdentityCredential,
AAD admin registration) is worth preserving:
- No connection string secrets to rotate
- Zero-trust posture consistent with the rest of the Azure setup
- Full parity with production in CI/CD and Bicep modules
Any external provider (Neon, Supabase, Railway) requires removing the MI auth code and substituting a password — a security regression and non-trivial code change for ~$7/mo saving over the B1ms target price.
When to reconsider Neon¶
If per-PR preview databases become a priority (see plan-api-backed-experience-skills.md
and pr-preview-envs backlog card), Neon's branching feature becomes a genuine advantage
that justifies the MI auth trade-off. At that point, Track B below is the implementation path.
Track B — Neon Migration (if chosen later)¶
What changes¶
| Layer | Change |
|---|---|
infra/modules/postgres.bicep |
Replace with neon_postgres.bicep — writes connection string to KV only |
infra/main.bicep |
Add @secure() param neon_database_url; swap module; remove MI params |
infra/modules/container_apps.bicep |
Remove USE_MANAGED_IDENTITY env var injection (~3 lines) |
api/db/engine.py |
Remove IMDS token-fetch branch; always use connect_args={'ssl': 'require'} |
deploy-infrastructure.yml |
Add --parameters neon_database_url="${{ secrets.NEON_DATABASE_URL }}" |
| GitHub Secrets | Add NEON_DATABASE_URL per environment (dev, prod) |
docker-compose.yml |
No change — local dev already uses password auth |
alembic/env.py |
No change — already reads DATABASE_URL from env |
New infra/modules/neon_postgres.bicep¶
// Neon Postgres module — writes connection string to Key Vault.
// Neon project/database are managed externally at console.neon.tech.
@secure()
param neon_database_url string
param key_vault_name string
resource key_vault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
name: key_vault_name
}
resource database_url_secret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = {
parent: key_vault
name: 'database-url'
properties: {
value: neon_database_url
contentType: 'text/plain'
}
}
Execution order¶
- Create Neon account at
console.neon.tech - Create project
portfolio, databaseportfolio, roleportfolio_app - Copy connection string:
postgresql+asyncpg://<role>:<password>@<host>/portfolio?ssl=require - Use
-poolerhostname suffix for built-in PgBouncer connection pooling - Add
NEON_DATABASE_URLas GitHub Secret ondevandprodenvironments - Create
infra/modules/neon_postgres.bicep - Update
infra/main.bicep— add param, swap module, remove MI params - Update
infra/modules/container_apps.bicep— removeUSE_MANAGED_IDENTITYinjection - Update
deploy-infrastructure.yml— addNEON_DATABASE_URLparam passthrough - Simplify
api/db/engine.py— remove MI branch, always use SSL auth - Run migrations against Neon before cutover:
- Deploy infra → containers redeploy → verify
/api/healthand DB-backed endpoints -
If prod data exists: dump and restore
-
Delete Azure Flexible Server; set
deploy_postgres = falsepermanently
Trade-offs¶
| Concern | Detail |
|---|---|
| No managed identity | Password stored in Key Vault. Rotation needs to be defined. |
| Cross-cloud latency | Neon runs on AWS Frankfurt. Adds ~5–20ms vs same-region Azure Postgres. |
| Double cold start | ACA + Neon both scale to zero on dev. ~3–4s combined cold start. Use -pooler hostname. |
| Cost at scale | Neon Launch $19/mo vs Azure B1ms ~$12/mo. Neon more expensive at higher utilisation. |
| Benefit | Detail |
|---|---|
| DB branching | Instant DB branches per PR — enables per-PR preview databases |
| Simpler auth code | Removes IMDS token-fetch complexity from engine.py |
| CI-accessible | Publicly reachable over SSL — migrations run from GitHub Actions without network tricks |
| Free tier for dev | Neon free tier sufficient for dev workloads |
| Connection pooler | Built-in PgBouncer via -pooler hostname suffix |