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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions