@forgrit/deploy-neon
v0.1.0
Published
Framework-agnostic Neon Postgres provisioner adapter. Project-level provision/destroy via the Neon Management API. Database provider — does NOT implement IDeploymentProvider (compute-shaped interface is wrong for databases). Sibling of @forgrit/deploy-cor
Maintainers
Readme
@forgrit/deploy-neon
Framework-agnostic Neon Postgres provisioner adapter. Project-level
provision / destroy via the Neon Management API. No NestJS, Prisma,
or app-framework runtime dependency.
Status: early-access (v0.x). v0.1.0 covers project-level provision + destroy. Branches, roles, endpoints, and pooled-vs-direct connection URIs are deferred to future v0.x.
Important: NOT a IDeploymentProvider
NeonProvider does NOT implement IDeploymentProvider from
@forgrit/deploy-core. The deploy-core interface is compute-shaped
— deploy / getStatus / teardown with status polling. Forcing a
database provider into that contract would mean stuffing connection
strings into deployUrl and faking a 'ready' poll.
NeonProvider ships its own narrow surface:
provision({ projectName }) → { projectId, databaseName, connectionString, raw }
destroy(projectId) → voidThis is the canonical "database adapter" shape in the @forgrit/deploy-*
family. Future database providers (@forgrit/deploy-supabase,
@forgrit/deploy-planetscale, etc.) will mirror this contract — or,
if multiple emerge, a shared IDatabaseProvider interface can be
extracted into @forgrit/[email protected].
Install
npm install @forgrit/deploy-neon @forgrit/deploy-core
# or
pnpm add @forgrit/deploy-neon @forgrit/deploy-coreQuick example
import { NeonProvider } from '@forgrit/deploy-neon';
const provider = new NeonProvider({
apiKey: process.env.NEON_API_KEY!,
});
// Create a project.
const { projectId, databaseName, connectionString } = await provider.provision({
projectName: 'my-org-my-app-1727481234567',
});
// `connectionString` is a full Neon Postgres URI (role + password + host +
// database) ready to pass straight to Prisma / pg / etc.
console.log(connectionString);
// Later, delete the project.
await provider.destroy(projectId);NestJS / orchestrator integration
This package intentionally has no NestJS, Prisma, or app-framework imports. Wire it into your orchestrator by passing config + logger hooks as constructor options.
import { Injectable, Logger } from '@nestjs/common';
import { randomUUID } from 'crypto';
import { NeonProvider as CoreNeonProvider, NeonConflictError } from '@forgrit/deploy-neon';
@Injectable()
export class MyNeonProvisioner {
private readonly logger = new Logger(MyNeonProvisioner.name);
private readonly delegate: CoreNeonProvider;
constructor(private readonly prisma: PrismaService) {
this.delegate = new CoreNeonProvider({
apiKey: () => process.env.NEON_API_KEY, // lazy — re-read per call
logger: {
log: (m, c) => this.logger.log(m, c),
warn: (m, c) => this.logger.warn(m, c),
error: (m, s, c) => this.logger.error(m, s, c),
},
});
}
async provision(orgId: string, appSlug: string) {
const projectName = `${orgId}-${appSlug}-${Date.now()}`;
try {
const result = await this.delegate.provision({ projectName });
const provisionId = `prov-${randomUUID()}`;
await this.prisma.dbProvisioning.create({
data: { provisionId /* ...result fields... */ },
});
return { provisionId, ...result };
} catch (err) {
if (err instanceof NeonConflictError) {
throw new MyAppConflictError(err.resourceRef);
}
throw err;
}
}
}The wrapper owns: project-name composition, provisionId minting,
Prisma row writes, NeonConflictError → app-specific error translation,
feature-flag fail-fast. The package owns ONLY the Neon REST surface.
API surface
| Export | Kind | Purpose |
| --------------------------------- | ----- | ----------------------------------------------------------------------------- |
| NeonProvider | class | The adapter — ships own narrow surface (not IDeploymentProvider) |
| NeonProviderOptions | type | Constructor argument shape |
| NeonProvisionResult | type | Return shape of provision({ projectName }) |
| NeonLogger | type | Optional log/warn/error hooks (all 3 methods optional) |
| NeonFetch | type | Injectable fetch shape with AbortSignal support |
| NeonConflictError | class | Thrown on 409 (name conflict). Carries resourceRef |
| NeonApiError | class | Thrown for other non-2xx + missing required fields. Carries status + body |
| NEON_DEFAULT_RETRY_DELAYS_MS | const | [250, 500, 1000] — retry budget for 429 / 5xx |
| NEON_DEFAULT_REQUEST_TIMEOUT_MS | const | 10000 — per-request timeout (AbortController) |
Behavior
| Method | What it does |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| provision({ projectName }) | POST /projects with { project: { name } }. Returns { projectId, databaseName, connectionString, raw }. databaseName defaults to 'neondb' if Neon's response omits it. Throws NeonApiError on missing project.id or connection_uris[0].connection_uri. Throws NeonConflictError on 409. Retries 429 / 5xx with exponential backoff. |
| destroy(projectId) | DELETE /projects/:id. Idempotent — 404 is treated as already-deleted (logged via logger?.warn, returns normally). Retries 429 / 5xx with exponential backoff. |
Network discipline
- Request timeout: 10s per attempt via
AbortController(override withrequestTimeoutMs). - Retry budget: 250 / 500 / 1000 ms for 429 / 5xx (override with
retryDelaysMs). Total attempts =delays.length + 1(default 4). - Typed errors: 409 →
NeonConflictError, 404 on destroy → idempotent return, other 4xx →NeonApiError, retryable exhausted →NeonApiError. - Lazy credentials:
apiKey+apiBaseUrlare read fresh on every call when passed as zero-arg getter functions.
v0.1.0 scope (capped at projects-only)
NOT included (deferred to future v0.x):
- Branch creation / deletion (
POST /projects/:id/branches) - Role / endpoint management
- Pooled vs direct connection URIs (currently returns
connection_uris[0]— whatever Neon's default is, usually direct) - Pro vs Free tier behavior differences
- GraphQL subscriptions / streaming logs
If your use case needs any of the above, open an issue — the next major
feature in @forgrit/[email protected] is dictated by demand.
Engines
- Node >= 20 (uses native
globalThis.fetch+AbortController). - Tests can pass an injected
fetchto run on older Node versions.
License
MIT. See LICENSE.
Issues: https://github.com/forgrit-ai/forgrit/issues
Related packages
@forgrit/deploy-core— provider-agnostic deployment primitives (IDeploymentProviderinterface for compute, failure taxonomy, retry helpers)@forgrit/deploy-vercel— Vercel compute provider (implementsIDeploymentProvider)@forgrit/deploy-railway— Railway compute provider (implementsIDeploymentProvider)
See plan #23b for design rationale + the load-bearing "no IDeploymentProvider" decision.
