@forgrit/deploy-railway
v0.1.0
Published
Framework-agnostic Railway deployment adapter implementing @forgrit/deploy-core. Hand-rolled GraphQL adapter for Railway's projectCreate/serviceCreate/deploymentCreate API. Bring your own NestJS/Express/orchestrator.
Downloads
172
Maintainers
Readme
@forgrit/deploy-railway
Framework-agnostic Railway deployment adapter implementing
IDeploymentProvider
from @forgrit/deploy-core. Hand-rolled GraphQL — no
graphql-request / @apollo/client runtime dependency.
Status: early-access (v0.x). v0.1.0 covers the standard Railway
3-mutation deploy sequence (projectCreate → serviceCreate →
deploymentCreate) + deployment status query + projectDelete
teardown. Composite deploymentId format
projectId:serviceId:deploymentId for full hierarchy tracking.
Install
npm install @forgrit/deploy-railway @forgrit/deploy-core
# or
pnpm add @forgrit/deploy-railway @forgrit/deploy-coreQuick example
import { RailwayProvider } from '@forgrit/deploy-railway';
const provider = new RailwayProvider({
token: process.env.RAILWAY_TOKEN!,
});
const result = await provider.deploy({
jobId: 'job-42',
workspacePath: '/path/to/workspace', // not directly uploaded; Railway pulls from Git
projectName: 'my-app',
});
console.log(result.deploymentId); // "projectId:serviceId:deploymentId"
// Poll until ready.
let status = await provider.getStatus(result.deploymentId);
while (status.status === 'queued' || status.status === 'building') {
await new Promise((r) => setTimeout(r, 2000));
status = await provider.getStatus(result.deploymentId);
}
// Teardown — deletes the whole Railway project.
await provider.teardown(result.deploymentId);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, BadRequestException } from '@nestjs/common';
import {
RailwayCompositeIdError,
RailwayProvider as CoreRailwayProvider,
} from '@forgrit/deploy-railway';
@Injectable()
export class MyRailwayProvider {
private readonly delegate: CoreRailwayProvider;
constructor(private readonly logger: MyLoggerService) {
this.delegate = new CoreRailwayProvider({
token: () => process.env.RAILWAY_TOKEN, // lazy
logger: this.logger,
});
}
async getStatus(deploymentId: string) {
try {
return await this.delegate.getStatus(deploymentId);
} catch (err) {
if (err instanceof RailwayCompositeIdError) {
// Re-throw as your framework's 400 type if needed.
throw new BadRequestException(err.message);
}
throw err;
}
}
}API surface
| Export | Kind | Purpose |
| ------------------------- | ----- | ------------------------------------------------------------------------------------------------- |
| RailwayProvider | class | The adapter — implements IDeploymentProvider |
| RailwayProviderOptions | type | Constructor argument shape |
| RailwayLogger | type | Optional log/warn/error hooks (all 3 methods optional) |
| RailwayDeployEventStore | type | Optional non-blocking event persistence (findLatest + recordTransition) |
| RailwayFetch | type | Injectable fetch shape (for testing without monkey-patching globals) |
| RailwayDeployEvent | type | Minimal event row shape (just { toState }) |
| RailwayDeployStatus | type | Alias for DeployStatusResult['status'] from @forgrit/deploy-core |
| RailwayCompositeId | type | Parsed { projectId, serviceId, deploymentId } |
| RailwayCompositeIdError | class | Thrown by parseRailwayCompositeId() for malformed inputs |
| parseRailwayCompositeId | fn | Pure helper — exported for diagnostics + framework-specific error translation |
| mapRailwayStatus | fn | Pure helper — Railway status → deploy-core status union (9-state mapping including cancelled) |
Behavior
| Method | What it does |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| deploy(params) | 3 sequential GraphQL mutations: projectCreate → serviceCreate → deploymentCreate. Returns { deploymentId: "projectId:serviceId:deploymentId", deployUrl, provider: 'railway', status: 'queued' }. Optionally records the initial queued transition (deduped if a prior event exists for deploymentId). |
| getStatus(deploymentId) | Parses the composite ID, runs deployment(id: $id) query against Railway. Maps status to the deploy-core status union. Optionally records state-change transitions; same-state polls are skipped. Backward-compat: also accepts raw deployment IDs (strings without :). |
| teardown(deploymentId) | Parses the composite ID, runs projectDelete(id: $id) mutation. Backward-compat: also accepts raw project IDs. |
Status mapping (9 states):
| Railway status | Mapped status |
| ----------------------- | ------------- |
| WAITING, QUEUED | queued |
| BUILDING, DEPLOYING | building |
| SUCCESS, RUNNING | ready |
| CRASHED, FAILED | error |
| REMOVED, CANCELLED | cancelled |
| <unknown> | error |
Composite deploymentId format
Railway's API hierarchy requires tracking three IDs (project, service,
deployment). Rather than forcing callers to manage 3 separate strings,
RailwayProvider returns a single colon-separated composite:
projectId:serviceId:deploymentIdgetStatus() parses out the deploymentId part for the deployment
query. teardown() parses out the projectId part for the
projectDelete mutation. Both methods accept either the composite form
OR a bare ID string (backward-compat with legacy callers).
Malformed composite IDs (wrong part count, empty parts) throw
RailwayCompositeIdError. NestJS / Express apps can catch this and
re-throw as BadRequestException (or equivalent 400-class error) to
keep the public API contract clean.
GraphQL transport
Railway's API is GraphQL-based. v0.1.0 hand-rolls the request via
globalThis.fetch (or your injected fetch) — no graphql-request /
@apollo/client / urql runtime dependency. Two-line POST helper does
the job; switching to a real GraphQL client is a future v0.x decision
if/when query/mutation complexity grows.
Errors are detected via TWO paths:
- Non-2xx HTTP responses → throws
Railway API error: <status> <body> - GraphQL-level errors array → throws
Railway GraphQL error: <first message>
Both paths surface enough information for retries + audit logging without leaking GraphQL-internal noise.
Engines
- Node >= 20 (uses native
globalThis.fetch). Tests can pass an injectedfetchto run on older Node versions.
License
MIT. See LICENSE.
Issues: https://github.com/forgrit-ai/forgrit/issues
Related packages
@forgrit/deploy-core— the provider contract this package implements@forgrit/deploy-vercel— sibling compute-provider adapter for Vercel@forgrit/deploy-neon(plan #23b, in flight) — Neon Postgres provisioner
See plan #23c for design rationale.
