A CommonGrants-compliant HTTP API that surfaces Pennsylvania eGrants data in a standard, interoperable format.
This project is both a proof of concept that demonstrates how a state grants portal can expose its data via the CommonGrants protocol, and a reference template for building CommonGrants API proxies against any source system.
The API fetches grant opportunity data from Pennsylvania's eGrants Beta API, normalizes it into the CommonGrants Opportunity schema (plus PA-specific custom fields), and serves it via standard CommonGrants endpoints. Data is kept fresh via a scheduled ETL that runs every 4 hours.
Default deployment: Cloudflare Workers + D1 (SQLite) + R2 (raw snapshots). Every layer is swappable — see PORTING.md for recipes.
┌─────────────────────────────────────────────────────────────────┐
│ src/index.ts (Workers entrypoint) │
│ ↓ buildConfig(env) │
│ src/cg.config.ts (wires adapter + storage + snapshots) │
│ ↓ │
│ src/app.ts (Hono factory; accepts AppConfig) │
├─────────────────────────────────────────────────────────────────┤
│ routes/ services/ etl/ │
│ ↓ ↓ ↓ │
│ ISourceClient · IOppRepo · ISnapshotStore │
├─────────────────────────────────────────────────────────────────┤
│ storage/ (pick one IOppRepo impl per deploy) │
│ ProxyOppRepo — tier 0, no persistence │
│ MemoryOppRepo — tier 1, single-instance cache (future) │
│ KvOppRepo — tier 2, serverless KV (future) │
│ SqliteOppRepo — tier 3, Kysely + D1/SQLite (default) │
│ │
│ snapshots/ │
│ BucketSnapshotStore — R2 / S3 / GCS │
│ NoopSnapshotStore — disabled │
├─────────────────────────────────────────────────────────────────┤
│ adapter/ (implements IAdapter<PaGrant>) │
│ PaAdapter — composed adapter object │
│ plugin.ts — definePlugin() → PaPlugin │
│ transform.ts — paGrantToOpportunity() pure fn │
│ PaSourceClient — ISourceClient for PA eGrants Beta │
│ (future: extract to @common-grants/cg-pa) │
└─────────────────────────────────────────────────────────────────┘
The IOppRepo interface supports all tiers; pick one in src/cg.config.ts.
| Tier | Repository impl | Storage | Search | Best for |
|---|---|---|---|---|
| 0 — Proxy | ProxyOppRepo |
None | Delegates to source API, or JS filter (fallback) | POC / demos / sources with native search / zero-ops |
| 1 — Memory | MemoryOppRepo (future) |
Process memory | JS filter | Node server, single instance, small data |
| 2 — KV | KvOppRepo (future) |
CF KV / Upstash | JS filter on blob | Serverless, read-heavy |
| 3 — SQL (default) | SqliteOppRepo |
D1 / SQLite via Kysely | SQL WHERE + FTS5 | Most production cases |
| 4 — Enterprise | PostgresOppRepo (future) |
Postgres / warehouse | SQL + tsvector / ES | Large data, strict security |
Routes, services, ETL, adapter, and the plugin layer are identical across all tiers — only which IOppRepo impl src/cg.config.ts wires changes.
See DEVELOPMENT.md for local setup and the dev workflow. Short version:
corepack enable
pnpm install
pnpm exec wrangler login # one-time
pnpm run bootstrap # idempotent: creates D1+R2, patches wrangler.jsonc, applies migrations
pnpm run devThen hit http://localhost:8787/docs.
No clickops policy. First-time setup is fully scripted. Don't click through the Cloudflare dashboard — if something's missing from
pnpm run bootstrap, add it there.
- TypeScript + Hono on Cloudflare Workers. Routes defined with
@hono/zod-openapiso the OpenAPI spec is auto-generated at/openapi.json. Docs UI at/docs(Scalar via CDN, no bundled dependency). - Schemas from
@common-grants/sdk. No handwritten opportunity schema, filters, or pagination envelope — the SDK provides them. - PA custom fields aligned with the CommonGrants custom fields catalog. Shared value schemas (
agency,contactInfo,additionalInfo,costSharing,legacySerialId) are mirrored from the grants.gov plugin for cross-plugin interoperability. - Auto-generated spec validated against the CommonGrants base protocol via
cg check specfrom@common-grants/cli. Runs in CI. - Auto-generated SQL types via
kysely-codegen. Never hand-editsrc/storage/sql/schema.ts. - No deep cross-directory imports. Every
src/<dir>/has anindex.tspublic surface. Lint-enforced.
This template is designed to be forked for any grants source. To adapt it:
- Replace
src/adapter/with an adapter for your source (plugin, transform, HTTP client). - Update resource names in
wrangler.jsoncwith your state/funder prefix. - Update
src/cg.config.tsto wire your adapter'sISourceClient. - Pick a deployment tier per PORTING.md.
See CONTRIBUTING.md.
See SECURITY.md.
MIT. Copyright © 2026 Agile Six Applications, Inc.