0
0
# Cloudflare Target Design
## Goal
Add `target: 'cloudflare'` to hatk so apps can deploy to Cloudflare Workers + Containers with D1 as the database. Same hatk project, different deployment target.
## Audience
hatk users who want edge deployment, scale-to-zero, or prefer Cloudflare over Railway/VPS.
## Architecture
When `hatk.config.ts` has `target: 'cloudflare'`, `hatk build` produces two deployment artifacts plus a generated Cloudflare config:
```
dist/
├── worker/ # Cloudflare Worker
│ └── index.ts # Fetch handler: XRPC + SvelteKit SSR + OAuth + admin
├── container/ # Cloudflare Container (Node process)
│ └── index.ts # Firehose subscription + backfill + label evaluation
└── wrangler.jsonc # Generated config (D1 binding, Service Binding, Container)
```
### Component Split
| Component | Runtime | Responsibilities |
|-----------|---------|-----------------|
| **Worker** | Cloudflare Worker | XRPC handlers, SvelteKit SSR, OAuth, admin API |
| **Container** | Cloudflare Container | Firehose, backfill, label evaluation |
| **D1** | Shared | All data storage (SQLite-compatible) |
### Communication
Worker → Container via **Service Binding RPC**. The Worker calls methods on the Container directly as function calls (near-zero latency, no HTTP overhead):
- `env.CONTAINER.resync(did)` — trigger backfill for a single repo
- `env.CONTAINER.resyncAll()` — trigger full re-enumeration
- `env.CONTAINER.getStatus()` — backfill progress
No Container → Worker communication needed.
---
## D1 Database Adapter
New file: `packages/hatk/src/database/adapters/d1.ts`
Implements the existing `DatabasePort` interface using Cloudflare's D1 binding API. No changes to the interface itself.
### Query Execution
- **`query(sql, params)`** — `d1.prepare(sql).bind(...params).all()`
- **`execute(sql, params)`** — `d1.prepare(sql).bind(...params).run()`
- **`executeMultiple(sql)`** — Split by `;`, run as `d1.batch([...])`
### Transactions
D1 has no `BEGIN/COMMIT/ROLLBACK`. The adapter fakes it:
1. `beginTransaction()` — start buffering statements
2. `execute()` during a transaction — append to buffer instead of executing
3. `commit()` — flush buffer as `d1.batch([...])` (atomic — all succeed or all fail)
4. `rollback()` — clear the buffer
This preserves the same atomicity guarantee as real transactions.
### Bulk Insert
The existing `BulkInserter` interface stays the same. The D1 implementation:
1. `append(values)` — generate an INSERT statement, add to batch buffer
2. `flush()` — send buffer as `d1.batch([...])`, clear buffer
3. Buffer size tuned to stay under D1's CPU limits per batch
No prepared statements or native appenders — all dynamic SQL generation.
### Dialect
D1 is SQLite under the hood. Reuse the SQLite dialect's type map and placeholder style. No new dialect flags — the D1 adapter handles its own constraints internally.
### FTS
D1 supports FTS5. The existing SQLite search port should work with D1 as-is.
---
## Config
```ts
// hatk.config.ts
export default defineConfig({
target: 'cloudflare', // new field, default: 'node'
// ... everything else stays the same
})
```
No other config changes. The `database` path field is ignored when target is `cloudflare` (D1 binding is configured in wrangler.jsonc).
---
## Worker Entry
Standard Cloudflare Workers fetch handler:
```ts
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// 1. Initialize D1 adapter from env.DB binding
// 2. Try XRPC routes (same handlers, D1-backed ctx)
// 3. Try admin routes (resync → env.CONTAINER.resync(did) via RPC)
// 4. Fall through to SvelteKit for everything else
}
}
```
XRPC handlers don't change — they receive `ctx` with `ctx.db.query()` backed by D1 instead of SQLite. OAuth is pure JS crypto, no native deps.
SvelteKit uses `@sveltejs/adapter-cloudflare` instead of `adapter-node`.
### Admin Resync Change
The only behavioral difference from the Node target: admin resync calls `env.CONTAINER.resync(did)` over RPC instead of `triggerAutoBackfill(did)` in-process.
---
## Container Entry
A standard Node process (Cloudflare Containers support Node). Essentially the current `main.ts` minus the HTTP server:
- Connects to firehose relay via WebSocket
- Processes commits, validates records, writes to D1
- Runs backfill loop for pending repos
- Exposes RPC methods for the Worker to call
No CPU/memory limits like Workers — it's a real container.
---
## Build Step
`hatk build` with `target: 'cloudflare'`:
1. Run `@sveltejs/adapter-cloudflare` for SvelteKit
2. Bundle XRPC handlers + OAuth into Worker entry
3. Bundle firehose + backfill into Container entry
4. Generate `wrangler.jsonc` with D1 binding, Service Binding to Container
User deploys with `npx wrangler deploy`.
---
## Known Limitations
- **D1 size limit**: 10GB per database. Cannot be increased. Sufficient for most hatk apps, but large-scale indexing may hit this. Document as a constraint of the Cloudflare target.
- **Backfill speed**: D1 writes are ~30ms (HTTP) vs sub-ms (local SQLite). Backfill will be noticeably slower on Cloudflare.
- **No horizontal sharding**: Single D1 database. Cloudflare's recommended pattern is per-tenant sharding, but that doesn't apply to firehose indexing.
---
## Implementation Scope
| Component | Estimated Lines | Description |
|-----------|----------------|-------------|
| D1 adapter | ~400 | `DatabasePort` implementation with batch transactions, bulk inserter |
| Dialect update | ~20 | Minimal — reuse SQLite dialect |
| Adapter factory | ~20 | Add `d1` engine selection |
| Worker entry | ~150 | Fetch handler wiring XRPC + SvelteKit + OAuth |
| Container entry | ~100 | Firehose + backfill + RPC methods |
| Build command | ~200 | `--target cloudflare` output generation + wrangler.jsonc |
| Config | ~10 | `target` field in `HatkConfig` |
| **Total** | **~900** | |