Skip to content

Evaluate Neon/PlanetScale + Hyperdrive for apps/host-cloudflare db seam (vs D1) #10

Description

@amondnet

Summary

Evaluate (and optionally implement) swapping apps/host-cloudflare's db seam
from D1 (SQLite) to Neon (or PlanetScale) + Hyperdrive. The same
repository already runs the Postgres-over-Hyperdrive path in production
(apps/cloud), so the swap target is a proven in-repo pattern rather than new
infrastructure.

Background

apps/host-cloudflare is the single-tenant self-host-on-Cloudflare template.
Its db seam is D1 (SQLite) via the shared FumaDB assembly (src/db/d1.ts,
README.md). FumaDB already supports postgresql/mysql providers
(packages/core/fumadb/src/shared/providers.ts), and apps/cloud already wires
postgres.js + Hyperdrive + a pg Drizzle schema (apps/cloud/wrangler.jsonc,
apps/cloud/src/db/db.ts).

Motivation: the D1 workarounds in src/db/d1.ts are the cost

Three documented D1 limitations are worked around today, all of which disappear
with Postgres/MySQL over Hyperdrive:

  1. interactiveTransactions: false (d1.ts:54) - D1 rejects interactive
    transactions, so multi-statement runtime writes (e.g. adding a source that
    derives many tools) auto-commit per statement and lose atomicity. This is a
    correctness issue, not just convenience. Postgres/MySQL restore real
    transactions.
  2. maxBoundParameters: 100 (d1.ts:61) - D1 caps bound parameters at 100,
    forcing createMany micro-batching for wide tables like tool. Postgres
    (up to 65535 params) removes the cap and the batching logic.
  3. ~1-2MB value cap -> R2 blob offload (d1.ts:71,
    data-migrations.ts copyGoogleOpenApiSpecBlobsToR2) - large OpenAPI specs /
    introspection snapshots are pushed to R2 to dodge D1's value ceiling.
    Postgres TOAST handles large values natively (R2 seam can stay for truly huge
    blobs / cost reasons, but the hard ceiling goes away).

Plus D1's 10GB-per-database cap and regional constraints are lifted by
Neon/PlanetScale.

Candidates (code effort is identical)

Both Neon and PlanetScale Postgres target FumaDB's postgresql provider and
reuse the exact postgres.js + pg Drizzle path apps/cloud already has
(apps/cloud/src/db/db.ts). Porting resolveConnectionString / makeSql and
switching provider: "sqlite" -> "postgresql" in d1.ts is the same low-
friction route for either. (An earlier note treated PlanetScale as MySQL/Vitess;
that objection does not apply to PlanetScale's Postgres product.) Both are
reached from the Worker via Hyperdrive. So the choice is operational/cost, not
code:

  • Neon - serverless, scale-to-zero, free tier, no minimum spend. Best fit
    for a low/spiky single-tenant template; idle cost trends to $0. Cold-start on
    resume (Hyperdrive caching partly mitigates).
  • PlanetScale Postgres - provisioned, always-on compute, no scale-to-zero.
    Predictable low latency / high sustained throughput, but pays even when idle.
    That profile is really apps/cloud's territory, not this template's.

Pricing comparison (early 2026, verify before deciding)

Option Free tier Paid from scale-to-zero Notes
D1 (current) 5GB / 150M reads / 3M writes per mo included in Workers Paid yes no external account/secret; incremental cost ~$0
Cloudflare Hyperdrive included free on Workers Paid ($5/mo) - pooling + caching $0 extra n/a required for external Postgres, but itself unmetered
Neon 100 CU-hours/mo, 0.5GB/project, 5GB egress Launch $0.106/CU-hour + $0.35/GB-mo storage, no minimum yes idle compute scales to zero
PlanetScale Postgres minimal/limited PS-5 $5/mo (512MB, 10GB incl.) / PS-10 $10/mo / HA $15-30 / Metal $50+ no always-on; storage overage $0.125/GB, egress 100GB then $0.06/GB

Cost ordering for this app's profile: D1 (~free, already on Workers) ≈ Neon
free tier < PlanetScale ($5+/mo always-on). On price alone, keeping D1 is the
floor; if an external Postgres is required, Neon is cheaper than PlanetScale for
low/spiky single-tenant traffic. Hyperdrive adds no cost beyond the Workers Paid
plan the deploy likely already has.

Recommendation

Neon + Hyperdrive if swapping. Same code effort as PlanetScale Postgres but
cheaper for this template's low/spiky, single-tenant profile (scale-to-zero +
free tier vs. always-on $5+/mo). Choose PlanetScale Postgres only if predictable
low latency / high sustained throughput becomes a hard requirement.

Trade-off to weigh

host-cloudflare's reason to exist is a zero-external-dependency single
Worker
self-host template (README.md: single-tenant, members/keys in
Cloudflare Access). D1 auto-provisions on wrangler deploy with no external
account or connection-string secret and is co-located with the Worker.
Neon/PlanetScale add an external account + secret + Hyperdrive setup. The swap
is worth it if D1 limits are being hit or transactional correctness matters;
otherwise D1 remains the right default for the template (large multi-tenant
scale is already apps/cloud's job).

Tasks

  • Decide: keep D1 default vs. swap vs. make the db seam configurable
  • If swapping: add a neon.ts / hyperdrive.ts DbProvider handle modeled
    on apps/cloud/src/db/db.ts (postgres.js + Hyperdrive)
  • Switch FumaDB provider sqlite -> postgresql; drop the three D1
    workarounds in d1.ts
  • Add hyperdrive binding to wrangler.jsonc; document connection-string
    secret + setup in README.md / RUNNING.md
  • Decide migration strategy (runtime ensure vs. out-of-band like cloud)
  • Keep or retire the R2 blob seam under Postgres

Additional Context

Originated from a code investigation of apps/host-cloudflare's storage layer.
Key references: apps/host-cloudflare/src/db/d1.ts,
apps/cloud/src/db/db.ts, apps/cloud/wrangler.jsonc,
packages/core/fumadb/src/shared/providers.ts.

Metadata

Metadata

Assignees

Labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions