diff --git a/alchemy-web/src/content/docs/guides/railway.mdx b/alchemy-web/src/content/docs/guides/railway.mdx
new file mode 100644
index 0000000000..dd2c6e13dd
--- /dev/null
+++ b/alchemy-web/src/content/docs/guides/railway.mdx
@@ -0,0 +1,215 @@
+---
+title: Railway
+description: Deploy applications and databases to Railway using Alchemy.
+sidebar:
+ order: 22
+---
+
+import { Steps, Tabs, TabItem } from "@astrojs/starlight/components";
+
+This guide walks you through deploying an application with a PostgreSQL database to Railway using Alchemy.
+
+
+
+0. Install and setup Alchemy
+
+
+
+
+ ```bash
+ bun add alchemy
+ ```
+
+
+
+
+ ```bash
+ npm install alchemy
+ ```
+
+
+
+
+ ```bash
+ pnpm add alchemy
+ ```
+
+
+
+
+ ```bash
+ yarn add alchemy
+ ```
+
+
+
+
+ :::tip
+ If you're new to Alchemy, see the [Getting Started](/getting-started/) guide.
+ :::
+
+1. **Get Railway API Token**
+
+ Create an API token from your [Railway account settings](https://railway.com/account/tokens).
+
+2. **Configure environment variables**
+
+ Add the token to your `.env` file:
+
+ ```bash
+ RAILWAY_API_TOKEN=your_railway_api_token
+ ```
+
+3. **Create your infrastructure**
+
+ Create `alchemy.run.ts` with a Railway project, service, and database:
+
+ ```ts title="alchemy.run.ts"
+ import alchemy from "alchemy";
+ import {
+ Project,
+ Service,
+ PostgresDatabase,
+ Domain,
+ Variable,
+ } from "alchemy/railway";
+
+ const app = await alchemy("my-railway-app");
+
+ // Create a Railway project
+ const project = await Project("my-project", {
+ name: "My App",
+ description: "My application on Railway",
+ });
+
+ // Deploy a PostgreSQL database
+ const db = await PostgresDatabase("db", {
+ project,
+ name: "postgres",
+ });
+
+ // Deploy the API service from Docker image
+ const api = await Service("api", {
+ project,
+ name: "api",
+ source: {
+ image: "nginx:alpine", // Replace with your image
+ },
+ });
+
+ // Get the production environment
+ const productionEnv = project.environments[0];
+
+ // Add database URL to the service
+ if (productionEnv) {
+ await Variable("db-url", {
+ project,
+ environment: productionEnv.id,
+ service: api,
+ name: "DATABASE_URL",
+ value: db.connectionUrl,
+ });
+
+ // Create a public domain
+ const domain = await Domain("api-domain", {
+ service: api,
+ environment: productionEnv.id,
+ });
+
+ console.log(`API will be available at: https://${domain.domain}`);
+ }
+
+ await app.finalize();
+ ```
+
+4. **Deploy from GitHub (Alternative)**
+
+ To deploy from a GitHub repository instead of a Docker image:
+
+ ```diff lang="ts" title="alchemy.run.ts"
+ const api = await Service("api", {
+ project,
+ name: "api",
+ - source: {
+ - image: "nginx:alpine",
+ - },
+ + source: {
+ + repo: "your-username/your-repo",
+ + branch: "main",
+ + },
+ + buildCommand: "npm run build",
+ + startCommand: "npm start",
+ });
+ ```
+
+5. **Deploy your application**
+
+ Use the Alchemy CLI to deploy:
+
+
+
+ ```sh
+ bun alchemy deploy
+ ```
+
+
+ ```sh
+ npx alchemy deploy
+ ```
+
+
+ ```sh
+ pnpm alchemy deploy
+ ```
+
+
+ ```sh
+ yarn alchemy deploy
+ ```
+
+
+
+ This will create your Railway project, database, and service. The console will output your service URL.
+
+6. **Verify deployment**
+
+ Visit the [Railway dashboard](https://railway.com) to see your deployed resources. You can also test your service by visiting the domain URL output in the previous step.
+
+7. **(Optional) Tear down**
+
+ Use the Alchemy CLI to delete all resources:
+
+
+
+ ```sh
+ bun alchemy destroy
+ ```
+
+
+ ```sh
+ npx alchemy destroy
+ ```
+
+
+ ```sh
+ pnpm alchemy destroy
+ ```
+
+
+ ```sh
+ yarn alchemy destroy
+ ```
+
+
+
+ :::caution
+ This will delete your database and all its data. Use `delete: false` on your database resource to preserve data.
+ :::
+
+
+
+## Next Steps
+
+- [Railway Provider Documentation](/providers/railway/) - Full API reference
+- [Railway Official Docs](https://docs.railway.com) - Learn more about Railway features
+- [Railway Templates](https://railway.app/templates) - Pre-built templates for common stacks
diff --git a/alchemy-web/src/content/docs/providers/railway/database.md b/alchemy-web/src/content/docs/providers/railway/database.md
new file mode 100644
index 0000000000..ad19046e2b
--- /dev/null
+++ b/alchemy-web/src/content/docs/providers/railway/database.md
@@ -0,0 +1,172 @@
+---
+title: Databases
+description: Learn how to deploy databases on Railway with Alchemy
+---
+
+Railway provides managed database services for PostgreSQL, MySQL, Redis, and MongoDB.
+
+## Authentication
+
+To use these resources, set `RAILWAY_API_TOKEN` in your `.env` file.
+
+## PostgreSQL
+
+### Basic Usage
+
+```ts
+import { Project, PostgresDatabase } from "alchemy/railway";
+
+const project = await Project("my-project", {
+ name: "My App",
+});
+
+const db = await PostgresDatabase("db", {
+ project,
+ name: "postgres",
+});
+
+// Use the connection URL
+console.log(db.connectionUrl); // Secret
+console.log(db.host); // hostname
+console.log(db.port); // 5432
+console.log(db.database); // database name
+console.log(db.user); // username
+console.log(db.password); // Secret
+```
+
+### With Service
+
+```ts
+import { Project, Service, PostgresDatabase, Variable } from "alchemy/railway";
+
+const project = await Project("my-project", {
+ name: "My App",
+});
+
+const db = await PostgresDatabase("db", { project });
+
+const api = await Service("api", {
+ project,
+ source: { repo: "myorg/api" },
+});
+
+// Connect the database to the service
+const productionEnv = project.environments[0];
+await Variable("db-url", {
+ project,
+ environment: productionEnv.id,
+ service: api,
+ name: "DATABASE_URL",
+ value: db.connectionUrl,
+});
+```
+
+## MySQL
+
+```ts
+import { Project, MysqlDatabase } from "alchemy/railway";
+
+const project = await Project("my-project", {
+ name: "My App",
+});
+
+const db = await MysqlDatabase("mysql", {
+ project,
+ name: "mysql-db",
+});
+
+console.log(db.connectionUrl); // mysql://...
+console.log(db.host);
+console.log(db.port); // 3306
+console.log(db.database);
+console.log(db.user);
+console.log(db.password);
+```
+
+## Redis
+
+```ts
+import { Project, RedisDatabase } from "alchemy/railway";
+
+const project = await Project("my-project", {
+ name: "My App",
+});
+
+const redis = await RedisDatabase("cache", {
+ project,
+ name: "redis-cache",
+});
+
+console.log(redis.connectionUrl); // redis://...
+console.log(redis.host);
+console.log(redis.port); // 6379
+console.log(redis.password);
+```
+
+## MongoDB
+
+```ts
+import { Project, MongoDatabase } from "alchemy/railway";
+
+const project = await Project("my-project", {
+ name: "My App",
+});
+
+const mongo = await MongoDatabase("mongo", {
+ project,
+ name: "mongodb",
+});
+
+console.log(mongo.connectionUrl); // mongodb://...
+```
+
+## Properties (All Databases)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `project` | `string \| Project` | The project to create the database in |
+| `environment` | `string \| Environment` | Target environment (optional) |
+| `name` | `string` | Name of the database service |
+| `delete` | `boolean` | Whether to delete on destroy (default: true) |
+
+## Outputs
+
+### PostgreSQL / MySQL
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `id` | `string` | The service ID |
+| `connectionUrl` | `Secret` | Full connection URL |
+| `host` | `string` | Database host |
+| `port` | `number` | Database port |
+| `database` | `string` | Database name |
+| `user` | `string` | Database user |
+| `password` | `Secret` | Database password |
+
+### Redis
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `id` | `string` | The service ID |
+| `connectionUrl` | `Secret` | Full connection URL |
+| `host` | `string` | Redis host |
+| `port` | `number` | Redis port (6379) |
+| `password` | `Secret` | Redis password |
+
+### MongoDB
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `id` | `string` | The service ID |
+| `connectionUrl` | `Secret` | Full connection URL |
+
+## Data Persistence
+
+By default, databases are deleted when removed from Alchemy. To preserve data:
+
+```ts
+const db = await PostgresDatabase("db", {
+ project,
+ delete: false, // Database will not be deleted
+});
+```
diff --git a/alchemy-web/src/content/docs/providers/railway/domain.md b/alchemy-web/src/content/docs/providers/railway/domain.md
new file mode 100644
index 0000000000..d8c9d52583
--- /dev/null
+++ b/alchemy-web/src/content/docs/providers/railway/domain.md
@@ -0,0 +1,95 @@
+---
+title: Domain
+description: Learn how to configure domains on Railway with Alchemy
+---
+
+Configure custom domains or Railway-generated domains for your services.
+
+## Authentication
+
+To use this resource, set `RAILWAY_API_TOKEN` in your `.env` file.
+
+## Examples
+
+### Railway-Generated Domain
+
+```ts
+import { Project, Service, Domain } from "alchemy/railway";
+
+const project = await Project("my-project", {
+ name: "My App",
+});
+
+const api = await Service("api", {
+ project,
+ source: { image: "nginx:alpine" },
+});
+
+const productionEnv = project.environments[0];
+
+const domain = await Domain("api-domain", {
+ service: api,
+ environment: productionEnv.id,
+});
+
+console.log(`Service available at: https://${domain.domain}`);
+// e.g., https://api-production-abc123.up.railway.app
+```
+
+### Custom Domain
+
+```ts
+import { Project, Service, Domain } from "alchemy/railway";
+
+const project = await Project("my-project", {
+ name: "My App",
+});
+
+const api = await Service("api", {
+ project,
+ source: { repo: "myorg/api" },
+});
+
+const productionEnv = project.environments[0];
+
+const domain = await Domain("custom-domain", {
+ service: api,
+ environment: productionEnv.id,
+ customDomain: "api.example.com",
+});
+
+// Don't forget to configure DNS!
+// Add a CNAME record pointing api.example.com to your Railway domain
+```
+
+## Properties
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `service` | `string \| Service` | The service to attach the domain to |
+| `environment` | `string \| Environment` | The target environment |
+| `customDomain` | `string` | Custom domain (e.g., "api.example.com") |
+| `delete` | `boolean` | Whether to delete on destroy (default: true) |
+
+## Outputs
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `id` | `string` | The domain ID |
+| `domain` | `string` | The domain name |
+| `serviceId` | `string` | Associated service ID |
+| `environmentId` | `string` | Associated environment ID |
+| `isCustom` | `boolean` | Whether this is a custom domain |
+| `targetPort` | `number` | Target port (if configured) |
+
+## DNS Configuration
+
+For custom domains, you need to configure DNS:
+
+1. **CNAME Record**: Point your custom domain to your Railway-generated domain
+2. **SSL**: Railway automatically provisions SSL certificates for custom domains
+
+Example DNS configuration:
+```
+api.example.com. CNAME api-production-abc123.up.railway.app.
+```
diff --git a/alchemy-web/src/content/docs/providers/railway/environment.md b/alchemy-web/src/content/docs/providers/railway/environment.md
new file mode 100644
index 0000000000..f5decc3b5b
--- /dev/null
+++ b/alchemy-web/src/content/docs/providers/railway/environment.md
@@ -0,0 +1,118 @@
+---
+title: Environment
+description: Learn how to create deployment environments on Railway with Alchemy
+---
+
+Create and manage deployment environments (staging, production, etc.) within a Railway project.
+
+## Authentication
+
+To use this resource, set `RAILWAY_API_TOKEN` in your `.env` file.
+
+## Examples
+
+### Create Environment
+
+```ts
+import { Project, Environment } from "alchemy/railway";
+
+const project = await Project("my-project", {
+ name: "My App",
+});
+
+const staging = await Environment("staging", {
+ project,
+ name: "staging",
+});
+
+const production = await Environment("production", {
+ project,
+ name: "production",
+});
+```
+
+### Copy from Existing Environment
+
+```ts
+const staging = await Environment("staging", {
+ project,
+ name: "staging",
+});
+
+// Create preview environment with staging's configuration
+const preview = await Environment("preview", {
+ project,
+ name: "preview",
+ sourceEnvironmentId: staging.id,
+});
+```
+
+### Reference Existing Environment
+
+```ts
+import { EnvironmentRef } from "alchemy/railway";
+
+const existingEnv = await EnvironmentRef({
+ environmentId: "your-environment-id",
+});
+```
+
+### Deploy to Specific Environment
+
+```ts
+import { Project, Environment, Service } from "alchemy/railway";
+
+const project = await Project("my-project", {
+ name: "My App",
+});
+
+const staging = await Environment("staging", {
+ project,
+ name: "staging",
+});
+
+const api = await Service("api", {
+ project,
+ environment: staging,
+ source: { repo: "myorg/api" },
+});
+```
+
+## Properties
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `project` | `string \| Project` | The project containing the environment |
+| `name` | `string` | Name of the environment |
+| `sourceEnvironmentId` | `string` | Environment to copy configuration from |
+| `delete` | `boolean` | Whether to delete on destroy (default: true) |
+
+## Outputs
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `id` | `string` | The environment ID |
+| `name` | `string` | Environment name |
+| `projectId` | `string` | Parent project ID |
+| `createdAt` | `string` | Creation timestamp |
+| `updatedAt` | `string` | Last update timestamp |
+
+## Default Environment
+
+Every Railway project comes with a default "production" environment. You can access it via the project:
+
+```ts
+const project = await Project("my-project", {
+ name: "My App",
+});
+
+const productionEnv = project.environments.find(
+ (env) => env.name === "production"
+);
+```
+
+## Best Practices
+
+1. **Consistent Naming**: Use consistent names like "staging", "production", "preview"
+2. **Environment Isolation**: Keep production and non-production environments separate
+3. **Copy Configuration**: Use `sourceEnvironmentId` to copy variables and settings
diff --git a/alchemy-web/src/content/docs/providers/railway/index.md b/alchemy-web/src/content/docs/providers/railway/index.md
new file mode 100644
index 0000000000..cc8d88971e
--- /dev/null
+++ b/alchemy-web/src/content/docs/providers/railway/index.md
@@ -0,0 +1,97 @@
+---
+title: Railway Provider
+description: Deploy and manage Railway resources using Alchemy
+---
+
+The Railway provider allows you to deploy applications, databases, and infrastructure to [Railway](https://railway.app), a modern platform-as-a-service. With this provider, you can create projects, deploy services from GitHub or Docker, provision databases, and configure networking.
+
+## Authentication
+
+Set `RAILWAY_API_TOKEN` in your `.env` file or pass `apiToken` directly in your resource configuration. You can create an API token from your [Railway account settings](https://railway.com/account/tokens).
+
+## Resources
+
+The Railway provider includes the following resources:
+
+- [Project](/providers/railway/project/) - Create and manage Railway projects
+- [Environment](/providers/railway/environment/) - Create deployment environments (staging, production, etc.)
+- [Service](/providers/railway/service/) - Deploy applications from GitHub repos or Docker images
+- [Domain](/providers/railway/domain/) - Configure custom or Railway-generated domains
+- [Variable](/providers/railway/variable/) - Manage environment variables
+- [Volume](/providers/railway/volume/) - Create persistent storage
+- [TcpProxy](/providers/railway/tcp-proxy/) - Enable TCP access for databases and other services
+- [PostgresDatabase](/providers/railway/database/) - Deploy PostgreSQL databases
+- [MysqlDatabase](/providers/railway/database/) - Deploy MySQL databases
+- [RedisDatabase](/providers/railway/database/) - Deploy Redis databases
+- [MongoDatabase](/providers/railway/database/) - Deploy MongoDB databases
+
+## Example
+
+Here's a complete example deploying a web application with a PostgreSQL database:
+
+```typescript
+import { alchemy } from "alchemy";
+import {
+ Project,
+ Service,
+ PostgresDatabase,
+ Domain,
+ Variable,
+} from "alchemy/railway";
+
+const app = await alchemy("my-app", {
+ stage: "production",
+});
+
+// Create a Railway project
+const project = await Project("my-project", {
+ name: "My Application",
+ description: "Production application",
+});
+
+// Deploy a PostgreSQL database
+const db = await PostgresDatabase("db", {
+ project,
+ name: "postgres",
+});
+
+// Deploy the API service from GitHub
+const api = await Service("api", {
+ project,
+ name: "api",
+ source: {
+ repo: "myorg/api",
+ branch: "main",
+ },
+ startCommand: "npm start",
+});
+
+// Get the production environment
+const productionEnv = project.environments.find((e) => e.name === "production");
+
+// Set the database URL as an environment variable
+if (productionEnv) {
+ await Variable("db-url", {
+ project,
+ environment: productionEnv.id,
+ service: api,
+ name: "DATABASE_URL",
+ value: db.connectionUrl,
+ });
+}
+
+// Add a domain
+const domain = await Domain("api-domain", {
+ service: api,
+ environment: productionEnv!.id,
+});
+
+console.log(`API deployed at: https://${domain.domain}`);
+
+await app.finalize();
+```
+
+## Additional Resources
+
+- [Railway Documentation](https://docs.railway.com)
+- [Railway API Reference](https://docs.railway.com/reference/public-api)
diff --git a/alchemy-web/src/content/docs/providers/railway/project.md b/alchemy-web/src/content/docs/providers/railway/project.md
new file mode 100644
index 0000000000..cbc16460b2
--- /dev/null
+++ b/alchemy-web/src/content/docs/providers/railway/project.md
@@ -0,0 +1,89 @@
+---
+title: Project
+description: Learn how to create and manage Railway Projects with Alchemy
+---
+
+Create and manage Railway projects. A project is the top-level container for your services, databases, and environments.
+
+## Authentication
+
+To use this resource, set `RAILWAY_API_TOKEN` in your `.env` file or pass `apiToken` directly in your resource configuration.
+
+## Examples
+
+### Basic Project
+
+```ts
+import { Project } from "alchemy/railway";
+
+const project = await Project("my-project", {
+ name: "My Application",
+ description: "Production application",
+});
+```
+
+### With API Token
+
+```ts
+import { Project } from "alchemy/railway";
+
+const project = await Project("my-project", {
+ apiToken: alchemy.secret(process.env.RAILWAY_API_TOKEN),
+ name: "My Application",
+});
+```
+
+### Adopt Existing Project
+
+```ts
+import { Project } from "alchemy/railway";
+
+const project = await Project("existing", {
+ adopt: true,
+ name: "existing-project-name",
+});
+```
+
+### Reference Existing Project
+
+```ts
+import { ProjectRef } from "alchemy/railway";
+
+const project = await ProjectRef({
+ projectId: "your-project-id",
+});
+```
+
+### Prevent Deletion
+
+```ts
+import { Project } from "alchemy/railway";
+
+const project = await Project("important", {
+ name: "Critical Production",
+ delete: false, // Won't be deleted when removed from Alchemy
+});
+```
+
+## Properties
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `name` | `string` | Name of the project |
+| `description` | `string` | Description of the project |
+| `isPublic` | `boolean` | Whether the project is public (default: false) |
+| `teamId` | `string` | Team ID for team projects |
+| `adopt` | `boolean` | Adopt an existing project by name |
+| `delete` | `boolean` | Whether to delete on destroy (default: true) |
+| `apiToken` | `Secret` | Railway API token |
+
+## Outputs
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `id` | `string` | The project ID |
+| `name` | `string` | Name of the project |
+| `description` | `string` | Description of the project |
+| `createdAt` | `string` | Creation timestamp |
+| `updatedAt` | `string` | Last update timestamp |
+| `environments` | `Array<{id, name}>` | Environments in the project |
diff --git a/alchemy-web/src/content/docs/providers/railway/service.md b/alchemy-web/src/content/docs/providers/railway/service.md
new file mode 100644
index 0000000000..4aac5c10c8
--- /dev/null
+++ b/alchemy-web/src/content/docs/providers/railway/service.md
@@ -0,0 +1,135 @@
+---
+title: Service
+description: Learn how to deploy services on Railway with Alchemy
+---
+
+Deploy applications from GitHub repositories or Docker images to Railway.
+
+## Authentication
+
+To use this resource, set `RAILWAY_API_TOKEN` in your `.env` file or pass `apiToken` directly in your resource configuration.
+
+## Examples
+
+### Deploy from GitHub
+
+```ts
+import { Project, Service } from "alchemy/railway";
+
+const project = await Project("my-project", {
+ name: "My App",
+});
+
+const api = await Service("api", {
+ project,
+ name: "api-service",
+ source: {
+ repo: "myorg/api",
+ branch: "main",
+ },
+});
+```
+
+### Deploy from Docker Image
+
+```ts
+import { Project, Service } from "alchemy/railway";
+
+const project = await Project("my-project", {
+ name: "My App",
+});
+
+const nginx = await Service("nginx", {
+ project,
+ name: "nginx",
+ source: {
+ image: "nginx:alpine",
+ },
+});
+```
+
+### With Build Configuration
+
+```ts
+import { Project, Service } from "alchemy/railway";
+
+const project = await Project("my-project", {
+ name: "My App",
+});
+
+const app = await Service("app", {
+ project,
+ source: {
+ repo: "myorg/monorepo",
+ branch: "main",
+ },
+ buildCommand: "npm run build",
+ startCommand: "npm start",
+ rootDirectory: "packages/api",
+});
+```
+
+### With Environment
+
+```ts
+import { Project, Environment, Service } from "alchemy/railway";
+
+const project = await Project("my-project", {
+ name: "My App",
+});
+
+const staging = await Environment("staging", {
+ project,
+ name: "staging",
+});
+
+const api = await Service("api", {
+ project,
+ environment: staging,
+ source: {
+ repo: "myorg/api",
+ },
+});
+```
+
+### With Health Checks
+
+```ts
+const api = await Service("api", {
+ project,
+ source: { image: "myorg/api:latest" },
+ healthcheckPath: "/health",
+ healthcheckTimeout: 30,
+ restartPolicyType: "ON_FAILURE",
+ restartPolicyMaxRetries: 3,
+});
+```
+
+## Properties
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `project` | `string \| Project` | The project to deploy to |
+| `environment` | `string \| Environment` | Target environment |
+| `name` | `string` | Name of the service |
+| `source.repo` | `string` | GitHub repository (e.g., "owner/repo") |
+| `source.branch` | `string` | Git branch to deploy |
+| `source.image` | `string` | Docker image to deploy |
+| `buildCommand` | `string` | Build command |
+| `startCommand` | `string` | Start command |
+| `rootDirectory` | `string` | Root directory for builds |
+| `numReplicas` | `number` | Number of replicas |
+| `healthcheckPath` | `string` | Health check endpoint |
+| `healthcheckTimeout` | `number` | Health check timeout (seconds) |
+| `restartPolicyType` | `string` | Restart policy: ON_FAILURE, ALWAYS, NEVER |
+| `restartPolicyMaxRetries` | `number` | Max restart retries |
+
+## Outputs
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `id` | `string` | The service ID |
+| `name` | `string` | Name of the service |
+| `projectId` | `string` | Parent project ID |
+| `createdAt` | `string` | Creation timestamp |
+| `domains` | `Array<{id, domain}>` | Associated domains |
diff --git a/alchemy-web/src/content/docs/providers/railway/tcp-proxy.md b/alchemy-web/src/content/docs/providers/railway/tcp-proxy.md
new file mode 100644
index 0000000000..41da327fb1
--- /dev/null
+++ b/alchemy-web/src/content/docs/providers/railway/tcp-proxy.md
@@ -0,0 +1,126 @@
+---
+title: TCP Proxy
+description: Learn how to configure TCP proxies on Railway with Alchemy
+---
+
+Enable external TCP access to your Railway services for databases and other non-HTTP protocols.
+
+## Authentication
+
+To use this resource, set `RAILWAY_API_TOKEN` in your `.env` file.
+
+## Examples
+
+### Database Access
+
+```ts
+import { Project, PostgresDatabase, TcpProxy } from "alchemy/railway";
+
+const project = await Project("my-project", {
+ name: "My App",
+});
+
+const db = await PostgresDatabase("db", { project });
+
+const productionEnv = project.environments[0];
+
+const proxy = await TcpProxy("db-proxy", {
+ service: db,
+ environment: productionEnv.id,
+ applicationPort: 5432,
+});
+
+console.log(`Connect to: ${proxy.domain}:${proxy.proxyPort}`);
+// e.g., monorail.proxy.rlwy.net:12345
+```
+
+### Redis Access
+
+```ts
+import { Project, RedisDatabase, TcpProxy } from "alchemy/railway";
+
+const project = await Project("my-project", {
+ name: "My App",
+});
+
+const redis = await RedisDatabase("cache", { project });
+
+const productionEnv = project.environments[0];
+
+const proxy = await TcpProxy("redis-proxy", {
+ service: redis,
+ environment: productionEnv.id,
+ applicationPort: 6379,
+});
+
+// Connect from external applications
+// redis-cli -h monorail.proxy.rlwy.net -p 12345 -a
+```
+
+### Custom Service
+
+```ts
+import { Project, Service, TcpProxy } from "alchemy/railway";
+
+const project = await Project("my-project", {
+ name: "My App",
+});
+
+const mqService = await Service("rabbitmq", {
+ project,
+ source: { image: "rabbitmq:3-management" },
+});
+
+const productionEnv = project.environments[0];
+
+// Expose AMQP port
+const amqpProxy = await TcpProxy("amqp-proxy", {
+ service: mqService,
+ environment: productionEnv.id,
+ applicationPort: 5672,
+});
+```
+
+## Properties
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `service` | `string \| Service` | The service to proxy |
+| `environment` | `string \| Environment` | The target environment |
+| `applicationPort` | `number` | The port on the service to forward to |
+| `delete` | `boolean` | Whether to delete on destroy (default: true) |
+
+## Outputs
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `id` | `string` | The TCP proxy ID |
+| `domain` | `string` | The proxy domain (e.g., monorail.proxy.rlwy.net) |
+| `proxyPort` | `number` | The external port to connect to |
+| `applicationPort` | `number` | The internal application port |
+| `serviceId` | `string` | Associated service ID |
+| `environmentId` | `string` | Associated environment ID |
+
+## Connection String
+
+To connect to your proxied service:
+
+```
+://:
+
+# PostgreSQL
+postgresql://user:pass@monorail.proxy.rlwy.net:12345/dbname
+
+# Redis
+redis://default:pass@monorail.proxy.rlwy.net:12346
+
+# MongoDB
+mongodb://user:pass@monorail.proxy.rlwy.net:12347/dbname
+```
+
+## Use Cases
+
+- **Database Access**: Connect to databases from local development
+- **Message Queues**: Expose RabbitMQ, Redis Pub/Sub, etc.
+- **Game Servers**: TCP-based game server connections
+- **Custom Protocols**: Any non-HTTP TCP service
diff --git a/alchemy-web/src/content/docs/providers/railway/variable.md b/alchemy-web/src/content/docs/providers/railway/variable.md
new file mode 100644
index 0000000000..99a715f326
--- /dev/null
+++ b/alchemy-web/src/content/docs/providers/railway/variable.md
@@ -0,0 +1,119 @@
+---
+title: Variable
+description: Learn how to manage environment variables on Railway with Alchemy
+---
+
+Manage environment variables for your Railway services.
+
+## Authentication
+
+To use this resource, set `RAILWAY_API_TOKEN` in your `.env` file.
+
+## Examples
+
+### Service-Specific Variable
+
+```ts
+import { Project, Service, Variable } from "alchemy/railway";
+
+const project = await Project("my-project", {
+ name: "My App",
+});
+
+const api = await Service("api", {
+ project,
+ source: { repo: "myorg/api" },
+});
+
+const productionEnv = project.environments[0];
+
+await Variable("api-key", {
+ project,
+ environment: productionEnv.id,
+ service: api,
+ name: "API_KEY",
+ value: alchemy.secret(process.env.API_KEY),
+});
+```
+
+### Shared Variable
+
+Variables without a service are shared across all services in the environment:
+
+```ts
+await Variable("shared-secret", {
+ project,
+ environment: productionEnv.id,
+ name: "SHARED_SECRET",
+ value: alchemy.secret(process.env.SHARED_SECRET),
+});
+```
+
+### Reference Variables
+
+Use Railway's reference syntax to link variables between services:
+
+```ts
+// Reference a variable from another service
+await Variable("redis-url", {
+ project,
+ environment: productionEnv.id,
+ service: api,
+ name: "REDIS_URL",
+ value: "${{Redis.REDIS_URL}}",
+});
+```
+
+### Database Connection
+
+```ts
+import { Project, Service, PostgresDatabase, Variable } from "alchemy/railway";
+
+const project = await Project("my-project", {
+ name: "My App",
+});
+
+const db = await PostgresDatabase("db", { project });
+
+const api = await Service("api", {
+ project,
+ source: { repo: "myorg/api" },
+});
+
+const productionEnv = project.environments[0];
+
+await Variable("database-url", {
+ project,
+ environment: productionEnv.id,
+ service: api,
+ name: "DATABASE_URL",
+ value: db.connectionUrl,
+});
+```
+
+## Properties
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `project` | `string \| Project` | The project containing the variable |
+| `environment` | `string \| Environment` | The target environment |
+| `service` | `string \| Service` | Service to attach to (optional for shared) |
+| `name` | `string` | Variable name (e.g., "DATABASE_URL") |
+| `value` | `string \| Secret` | Variable value |
+| `delete` | `boolean` | Whether to delete on destroy (default: true) |
+
+## Outputs
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `name` | `string` | Variable name |
+| `value` | `Secret` | Variable value (wrapped) |
+| `projectId` | `string` | Project ID |
+| `environmentId` | `string` | Environment ID |
+| `serviceId` | `string` | Service ID (if service-specific) |
+
+## Best Practices
+
+1. **Use Secrets**: Always wrap sensitive values with `alchemy.secret()`
+2. **Service-Specific**: Prefer service-specific variables over shared when possible
+3. **Reference Variables**: Use Railway's reference syntax for cross-service dependencies
diff --git a/alchemy-web/src/content/docs/providers/railway/volume.md b/alchemy-web/src/content/docs/providers/railway/volume.md
new file mode 100644
index 0000000000..20866c51b2
--- /dev/null
+++ b/alchemy-web/src/content/docs/providers/railway/volume.md
@@ -0,0 +1,95 @@
+---
+title: Volume
+description: Learn how to create persistent storage on Railway with Alchemy
+---
+
+Create persistent storage volumes for your Railway services.
+
+## Authentication
+
+To use this resource, set `RAILWAY_API_TOKEN` in your `.env` file.
+
+## Examples
+
+### Basic Volume
+
+```ts
+import { Project, Service, Volume } from "alchemy/railway";
+
+const project = await Project("my-project", {
+ name: "My App",
+});
+
+const api = await Service("api", {
+ project,
+ source: { image: "node:20" },
+});
+
+const productionEnv = project.environments[0];
+
+const uploads = await Volume("uploads", {
+ project,
+ service: api,
+ environment: productionEnv.id,
+ mountPath: "/app/uploads",
+});
+```
+
+### Database Storage
+
+```ts
+const dbService = await Service("postgres", {
+ project,
+ source: { image: "postgres:16" },
+});
+
+const dataVolume = await Volume("postgres-data", {
+ project,
+ service: dbService,
+ environment: productionEnv.id,
+ name: "postgres-data",
+ mountPath: "/var/lib/postgresql/data",
+});
+```
+
+### Preserve Data on Destroy
+
+```ts
+const volume = await Volume("important-data", {
+ project,
+ service: api,
+ environment: productionEnv.id,
+ mountPath: "/data",
+ delete: false, // Volume won't be deleted when removed from Alchemy
+});
+```
+
+## Properties
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `project` | `string \| Project` | The project containing the volume |
+| `service` | `string \| Service` | The service to attach the volume to |
+| `environment` | `string \| Environment` | The target environment |
+| `name` | `string` | Name of the volume |
+| `mountPath` | `string` | Path where the volume is mounted in the container |
+| `delete` | `boolean` | Whether to delete on destroy (default: true) |
+
+## Outputs
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `id` | `string` | The volume ID |
+| `name` | `string` | Volume name |
+| `mountPath` | `string` | Mount path |
+| `projectId` | `string` | Project ID |
+| `serviceId` | `string` | Service ID |
+| `environmentId` | `string` | Environment ID |
+| `createdAt` | `string` | Creation timestamp |
+
+## Important Notes
+
+- **Data Persistence**: Volumes persist data across deployments
+- **Single Service**: Each volume can only be attached to one service
+- **Mount Path Changes**: Changing the mount path requires volume replacement
+- **Data Loss Warning**: Deleting a volume permanently deletes all data
diff --git a/alchemy/package.json b/alchemy/package.json
index c26678aa7d..e89b552d14 100644
--- a/alchemy/package.json
+++ b/alchemy/package.json
@@ -139,6 +139,10 @@
"bun": "./src/prisma-postgres/index.ts",
"import": "./lib/prisma-postgres/index.js"
},
+ "./railway": {
+ "bun": "./src/railway/index.ts",
+ "import": "./lib/railway/index.js"
+ },
"./random": {
"bun": "./src/random/index.ts",
"import": "./lib/random/index.js"
diff --git a/alchemy/src/railway/api.ts b/alchemy/src/railway/api.ts
new file mode 100644
index 0000000000..3e300ac14d
--- /dev/null
+++ b/alchemy/src/railway/api.ts
@@ -0,0 +1,141 @@
+import type { Secret } from "../secret.ts";
+
+/**
+ * Options for Railway API requests
+ */
+export interface RailwayApiOptions {
+ /**
+ * Base URL for Railway GraphQL API
+ * @default https://backboard.railway.com/graphql/v2
+ */
+ baseUrl?: string;
+
+ /**
+ * API token to use (overrides RAILWAY_API_TOKEN env var)
+ */
+ apiToken?: Secret;
+}
+
+/**
+ * Error from Railway API
+ */
+export class RailwayError extends Error {
+ code: string;
+ status: number;
+
+ constructor(props: { message: string; code?: string; status: number }) {
+ super(`Railway API error (${props.status}): ${props.message}`);
+ this.code = props.code ?? "UNKNOWN";
+ this.status = props.status;
+ }
+}
+
+/**
+ * GraphQL error shape from Railway API
+ */
+interface GraphQLError {
+ message: string;
+ extensions?: {
+ code?: string;
+ };
+}
+
+/**
+ * GraphQL response shape
+ */
+interface GraphQLResponse {
+ data?: T;
+ errors?: GraphQLError[];
+}
+
+/**
+ * Railway API client for GraphQL operations
+ */
+export interface RailwayClient {
+ /**
+ * Execute a GraphQL query or mutation
+ */
+ request(query: string, variables?: Record): Promise;
+}
+
+/**
+ * Create a Railway API client with environment variable fallback
+ * @param options API options
+ * @returns Railway API client instance
+ */
+export function createRailwayApi(
+ options: Partial = {},
+): RailwayClient {
+ const apiToken =
+ options.apiToken?.unencrypted ?? process.env.RAILWAY_API_TOKEN;
+ if (!apiToken) {
+ throw new Error(
+ "Railway API token is required. Set RAILWAY_API_TOKEN environment variable or provide apiToken option.",
+ );
+ }
+
+ const baseUrl = options.baseUrl ?? "https://backboard.railway.com/graphql/v2";
+
+ return {
+ async request(
+ query: string,
+ variables?: Record,
+ ): Promise {
+ const response = await fetch(baseUrl, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${apiToken}`,
+ },
+ body: JSON.stringify({
+ query,
+ variables,
+ }),
+ });
+
+ if (!response.ok) {
+ throw new RailwayError({
+ message: `HTTP error: ${response.statusText}`,
+ status: response.status,
+ });
+ }
+
+ const json = (await response.json()) as GraphQLResponse;
+
+ if (json.errors && json.errors.length > 0) {
+ const firstError = json.errors[0];
+ throw new RailwayError({
+ message: firstError.message,
+ code: firstError.extensions?.code,
+ status: 400,
+ });
+ }
+
+ if (!json.data) {
+ throw new RailwayError({
+ message: "No data returned from Railway API",
+ status: 500,
+ });
+ }
+
+ return json.data;
+ },
+ };
+}
+
+/**
+ * Helper to extract a specific field from a GraphQL response
+ */
+export function extractResult(
+ data: Record,
+ field: string,
+): T {
+ const result = data[field];
+ if (result === undefined) {
+ throw new RailwayError({
+ message: `Expected field '${field}' not found in response`,
+ status: 500,
+ });
+ }
+ return result as T;
+}
diff --git a/alchemy/src/railway/domain.ts b/alchemy/src/railway/domain.ts
new file mode 100644
index 0000000000..a66d8932fd
--- /dev/null
+++ b/alchemy/src/railway/domain.ts
@@ -0,0 +1,270 @@
+import type { Context } from "../context.ts";
+import { Resource, ResourceKind } from "../resource.ts";
+import {
+ createRailwayApi,
+ type RailwayApiOptions,
+ RailwayError,
+} from "./api.ts";
+import { isEnvironment, type Environment } from "./environment.ts";
+import { isService, type Service } from "./service.ts";
+
+/**
+ * Properties for creating a Railway domain
+ */
+export interface DomainProps extends RailwayApiOptions {
+ /**
+ * The service to attach the domain to
+ */
+ service: string | Service;
+
+ /**
+ * The environment to attach the domain to
+ */
+ environment: string | Environment;
+
+ /**
+ * Custom domain to use (e.g., "api.example.com")
+ * If not provided, a Railway-generated domain will be created
+ */
+ customDomain?: string;
+
+ /**
+ * Whether to delete the domain when the resource is destroyed.
+ * @default true
+ */
+ delete?: boolean;
+}
+
+/**
+ * Output returned after Railway domain creation
+ */
+export interface Domain {
+ /**
+ * The ID of the domain
+ */
+ id: string;
+
+ /**
+ * The domain name
+ */
+ domain: string;
+
+ /**
+ * The service ID this domain is attached to
+ */
+ serviceId: string;
+
+ /**
+ * The environment ID this domain is attached to
+ */
+ environmentId: string;
+
+ /**
+ * Whether this is a custom domain
+ */
+ isCustom: boolean;
+
+ /**
+ * Target port for the domain (for custom domains)
+ */
+ targetPort?: number;
+}
+
+/**
+ * GraphQL response types
+ * @internal
+ */
+interface ServiceDomainCreateResponse {
+ serviceDomainCreate: {
+ id: string;
+ domain: string;
+ serviceId: string;
+ environmentId: string;
+ targetPort?: number;
+ };
+}
+
+interface CustomDomainCreateResponse {
+ customDomainCreate: {
+ id: string;
+ domain: string;
+ serviceId: string;
+ environmentId: string;
+ targetPort?: number;
+ };
+}
+
+/**
+ * Type guard for Railway Domain
+ */
+export function isDomain(resource: any): resource is Domain {
+ return resource?.[ResourceKind] === "railway::Domain";
+}
+
+/**
+ * Get the service ID from a service reference
+ */
+function getServiceId(service: string | Service): string {
+ if (isService(service)) {
+ return service.id;
+ }
+ return service;
+}
+
+/**
+ * Get the environment ID from an environment reference
+ */
+function getEnvironmentId(environment: string | Environment): string {
+ if (isEnvironment(environment)) {
+ return environment.id;
+ }
+ return environment;
+}
+
+/**
+ * Creates a Railway domain for accessing a service.
+ *
+ * @example
+ * // Create a Railway-generated domain:
+ * const domain = await Domain("api-domain", {
+ * service: apiService,
+ * environment: production
+ * });
+ * console.log(`API available at: https://${domain.domain}`);
+ *
+ * @example
+ * // Create a custom domain:
+ * const domain = await Domain("custom-domain", {
+ * service: apiService,
+ * environment: production,
+ * customDomain: "api.example.com"
+ * });
+ */
+export const Domain = Resource(
+ "railway::Domain",
+ async function (
+ this: Context,
+ id: string,
+ props: DomainProps,
+ ): Promise {
+ const api = createRailwayApi(props);
+ const serviceId = getServiceId(props.service);
+ const environmentId = getEnvironmentId(props.environment);
+
+ if (this.phase === "delete") {
+ if (props.delete !== false && this.output?.id) {
+ try {
+ if (this.output.isCustom) {
+ await api.request(
+ `
+ mutation customDomainDelete($id: String!) {
+ customDomainDelete(id: $id)
+ }
+ `,
+ { id: this.output.id },
+ );
+ } else {
+ await api.request(
+ `
+ mutation serviceDomainDelete($id: String!) {
+ serviceDomainDelete(id: $id)
+ }
+ `,
+ { id: this.output.id },
+ );
+ }
+ } catch (error) {
+ // Ignore 404 errors (domain already deleted)
+ if (error instanceof RailwayError && error.code === "NOT_FOUND") {
+ // Already deleted, that's fine
+ } else {
+ throw error;
+ }
+ }
+ }
+ return this.destroy();
+ }
+
+ // Domains are immutable - changing domain type requires replacement
+ if (this.phase === "update" && this.output?.id) {
+ const currentIsCustom = this.output.isCustom;
+ const newIsCustom = !!props.customDomain;
+
+ // If switching between custom and generated, replace
+ if (currentIsCustom !== newIsCustom) {
+ return this.replace();
+ }
+
+ // If custom domain name changed, replace
+ if (newIsCustom && this.output.domain !== props.customDomain) {
+ return this.replace();
+ }
+
+ // No changes needed
+ return this.output;
+ }
+
+ // Create custom domain
+ if (props.customDomain) {
+ const data = await api.request(
+ `
+ mutation customDomainCreate($input: CustomDomainCreateInput!) {
+ customDomainCreate(input: $input) {
+ id
+ domain
+ serviceId
+ environmentId
+ targetPort
+ }
+ }
+ `,
+ {
+ input: {
+ domain: props.customDomain,
+ serviceId,
+ environmentId,
+ },
+ },
+ );
+
+ return {
+ id: data.customDomainCreate.id,
+ domain: data.customDomainCreate.domain,
+ serviceId: data.customDomainCreate.serviceId,
+ environmentId: data.customDomainCreate.environmentId,
+ isCustom: true,
+ targetPort: data.customDomainCreate.targetPort,
+ };
+ }
+
+ // Create Railway-generated domain
+ const data = await api.request(
+ `
+ mutation serviceDomainCreate($input: ServiceDomainCreateInput!) {
+ serviceDomainCreate(input: $input) {
+ id
+ domain
+ serviceId
+ environmentId
+ targetPort
+ }
+ }
+ `,
+ {
+ input: {
+ serviceId,
+ environmentId,
+ },
+ },
+ );
+
+ return {
+ id: data.serviceDomainCreate.id,
+ domain: data.serviceDomainCreate.domain,
+ serviceId: data.serviceDomainCreate.serviceId,
+ environmentId: data.serviceDomainCreate.environmentId,
+ isCustom: false,
+ targetPort: data.serviceDomainCreate.targetPort,
+ };
+ },
+);
diff --git a/alchemy/src/railway/environment.ts b/alchemy/src/railway/environment.ts
new file mode 100644
index 0000000000..1edde45b5a
--- /dev/null
+++ b/alchemy/src/railway/environment.ts
@@ -0,0 +1,237 @@
+import type { Context } from "../context.ts";
+import { Resource, ResourceKind } from "../resource.ts";
+import {
+ createRailwayApi,
+ type RailwayApiOptions,
+ RailwayError,
+} from "./api.ts";
+import { isProject, type Project } from "./project.ts";
+
+/**
+ * Properties for creating or updating a Railway environment
+ */
+export interface EnvironmentProps extends RailwayApiOptions {
+ /**
+ * The project to create the environment in
+ */
+ project: string | Project;
+
+ /**
+ * Name of the environment
+ *
+ * @default ${app}-${stage}-${id}
+ */
+ name?: string;
+
+ /**
+ * Source environment ID to copy configuration from
+ * If not provided, creates an empty environment
+ */
+ sourceEnvironmentId?: string;
+
+ /**
+ * Whether to delete the environment when the resource is destroyed.
+ * @default true
+ */
+ delete?: boolean;
+}
+
+/**
+ * Output returned after Railway environment creation/update
+ */
+export interface Environment {
+ /**
+ * The ID of the environment
+ */
+ id: string;
+
+ /**
+ * Name of the environment
+ */
+ name: string;
+
+ /**
+ * The project ID this environment belongs to
+ */
+ projectId: string;
+
+ /**
+ * Time at which the environment was created
+ */
+ createdAt: string;
+
+ /**
+ * Time at which the environment was last updated
+ */
+ updatedAt: string;
+}
+
+/**
+ * GraphQL response types
+ * @internal
+ */
+interface EnvironmentCreateResponse {
+ environmentCreate: {
+ id: string;
+ name: string;
+ projectId: string;
+ createdAt: string;
+ updatedAt: string;
+ };
+}
+
+interface EnvironmentQueryResponse {
+ environment: {
+ id: string;
+ name: string;
+ projectId: string;
+ createdAt: string;
+ updatedAt: string;
+ };
+}
+
+const ENVIRONMENT_FIELDS = `
+ id
+ name
+ projectId
+ createdAt
+ updatedAt
+`;
+
+/**
+ * Type guard for Railway Environment
+ */
+export function isEnvironment(resource: any): resource is Environment {
+ return resource?.[ResourceKind] === "railway::Environment";
+}
+
+/**
+ * Get the project ID from a project reference
+ */
+function getProjectId(project: string | Project): string {
+ if (isProject(project)) {
+ return project.id;
+ }
+ return project;
+}
+
+/**
+ * Creates a Railway environment for deploying services to different stages.
+ *
+ * @example
+ * // Create a staging environment:
+ * const staging = await Environment("staging", {
+ * project: project,
+ * name: "staging"
+ * });
+ *
+ * @example
+ * // Create an environment from an existing one:
+ * const preview = await Environment("preview", {
+ * project: project,
+ * name: "preview",
+ * sourceEnvironmentId: production.id
+ * });
+ */
+export const Environment = Resource(
+ "railway::Environment",
+ async function (
+ this: Context,
+ id: string,
+ props: EnvironmentProps,
+ ): Promise {
+ const api = createRailwayApi(props);
+ const projectId = getProjectId(props.project);
+ const name =
+ props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);
+
+ if (this.phase === "delete") {
+ if (props.delete !== false && this.output?.id) {
+ try {
+ await api.request(
+ `
+ mutation environmentDelete($id: String!) {
+ environmentDelete(id: $id)
+ }
+ `,
+ { id: this.output.id },
+ );
+ } catch (error) {
+ // Ignore 404 errors (environment already deleted)
+ if (error instanceof RailwayError && error.code === "NOT_FOUND") {
+ // Already deleted, that's fine
+ } else {
+ throw error;
+ }
+ }
+ }
+ return this.destroy();
+ }
+
+ // Updates to environments are limited - name changes require delete/create
+ if (this.phase === "update" && this.output?.id) {
+ // Check if name changed (requires replacement)
+ if (this.output.name !== name) {
+ return this.replace();
+ }
+ // No other updatable fields, return current state
+ return this.output;
+ }
+
+ // Create new environment
+ const data = await api.request(
+ `
+ mutation environmentCreate($input: EnvironmentCreateInput!) {
+ environmentCreate(input: $input) {
+ ${ENVIRONMENT_FIELDS}
+ }
+ }
+ `,
+ {
+ input: {
+ name,
+ projectId,
+ sourceEnvironmentId: props.sourceEnvironmentId,
+ },
+ },
+ );
+
+ const env = data.environmentCreate;
+ return {
+ id: env.id,
+ name: env.name,
+ projectId: env.projectId,
+ createdAt: env.createdAt,
+ updatedAt: env.updatedAt,
+ };
+ },
+);
+
+/**
+ * Reference an existing Railway environment by ID
+ */
+export async function EnvironmentRef(
+ props: RailwayApiOptions & { environmentId: string },
+): Promise {
+ const api = createRailwayApi(props);
+
+ const data = await api.request(
+ `
+ query environment($id: String!) {
+ environment(id: $id) {
+ ${ENVIRONMENT_FIELDS}
+ }
+ }
+ `,
+ { id: props.environmentId },
+ );
+
+ const env = data.environment;
+ return {
+ id: env.id,
+ name: env.name,
+ projectId: env.projectId,
+ createdAt: env.createdAt,
+ updatedAt: env.updatedAt,
+ };
+}
diff --git a/alchemy/src/railway/index.ts b/alchemy/src/railway/index.ts
new file mode 100644
index 0000000000..20f192ef10
--- /dev/null
+++ b/alchemy/src/railway/index.ts
@@ -0,0 +1,63 @@
+export {
+ createRailwayApi,
+ RailwayError,
+ type RailwayApiOptions,
+ type RailwayClient,
+} from "./api.ts";
+
+export {
+ Project,
+ ProjectRef,
+ isProject,
+ type ProjectProps,
+ type Project as ProjectOutput,
+ type ProjectEnvironment,
+} from "./project.ts";
+
+export {
+ Environment,
+ EnvironmentRef,
+ isEnvironment,
+ type EnvironmentProps,
+ type Environment as EnvironmentOutput,
+} from "./environment.ts";
+
+export {
+ Service,
+ ServiceRef,
+ isService,
+ type ServiceProps,
+ type Service as ServiceOutput,
+ type ServiceDomain,
+ type GitHubSource,
+ type DockerSource,
+} from "./service.ts";
+
+export {
+ Domain,
+ isDomain,
+ type DomainProps,
+ type Domain as DomainOutput,
+} from "./domain.ts";
+
+export {
+ Variable,
+ getVariables,
+ isVariable,
+ type VariableProps,
+ type Variable as VariableOutput,
+} from "./variable.ts";
+
+export {
+ Volume,
+ isVolume,
+ type VolumeProps,
+ type Volume as VolumeOutput,
+} from "./volume.ts";
+
+export {
+ TcpProxy,
+ isTcpProxy,
+ type TcpProxyProps,
+ type TcpProxy as TcpProxyOutput,
+} from "./tcp-proxy.ts";
diff --git a/alchemy/src/railway/project.ts b/alchemy/src/railway/project.ts
new file mode 100644
index 0000000000..a84a67aab3
--- /dev/null
+++ b/alchemy/src/railway/project.ts
@@ -0,0 +1,431 @@
+import type { Context } from "../context.ts";
+import { Resource, ResourceKind } from "../resource.ts";
+import {
+ createRailwayApi,
+ extractResult,
+ type RailwayApiOptions,
+ RailwayError,
+} from "./api.ts";
+
+/**
+ * Properties for creating or updating a Railway project
+ */
+export interface ProjectProps extends RailwayApiOptions {
+ /**
+ * When `true`, will adopt an existing project by `name`
+ */
+ adopt?: true;
+
+ /**
+ * Whether to delete the project when the resource is destroyed.
+ * @default true, unless the resource was adopted
+ */
+ delete?: boolean;
+
+ /**
+ * Name of the project
+ *
+ * @default ${app}-${stage}-${id}
+ */
+ name?: string;
+
+ /**
+ * Description of the project
+ */
+ description?: string;
+
+ /**
+ * Whether the project is public
+ * @default false
+ */
+ isPublic?: boolean;
+
+ /**
+ * Team ID to create the project in (for team tokens)
+ */
+ teamId?: string;
+}
+
+/**
+ * A Railway environment within a project
+ */
+export interface ProjectEnvironment {
+ id: string;
+ name: string;
+}
+
+/**
+ * Output returned after Railway project creation/update
+ */
+export interface Project {
+ /**
+ * The ID of the project
+ */
+ id: string;
+
+ /**
+ * Name of the project
+ */
+ name: string;
+
+ /**
+ * Description of the project
+ */
+ description: string;
+
+ /**
+ * Whether the project is public
+ */
+ isPublic: boolean;
+
+ /**
+ * Time at which the project was created
+ */
+ createdAt: string;
+
+ /**
+ * Time at which the project was last updated
+ */
+ updatedAt: string;
+
+ /**
+ * Environments in the project
+ */
+ environments: ProjectEnvironment[];
+
+ /**
+ * Team ID if the project belongs to a team
+ */
+ teamId?: string;
+}
+
+/**
+ * GraphQL response types
+ * @internal
+ */
+interface ProjectCreateResponse {
+ projectCreate: {
+ id: string;
+ name: string;
+ description: string;
+ isPublic: boolean;
+ createdAt: string;
+ updatedAt: string;
+ teamId?: string;
+ environments: {
+ edges: Array<{
+ node: {
+ id: string;
+ name: string;
+ };
+ }>;
+ };
+ };
+}
+
+interface ProjectUpdateResponse {
+ projectUpdate: {
+ id: string;
+ name: string;
+ description: string;
+ isPublic: boolean;
+ createdAt: string;
+ updatedAt: string;
+ teamId?: string;
+ environments: {
+ edges: Array<{
+ node: {
+ id: string;
+ name: string;
+ };
+ }>;
+ };
+ };
+}
+
+interface ProjectQueryResponse {
+ project: {
+ id: string;
+ name: string;
+ description: string;
+ isPublic: boolean;
+ createdAt: string;
+ updatedAt: string;
+ teamId?: string;
+ environments: {
+ edges: Array<{
+ node: {
+ id: string;
+ name: string;
+ };
+ }>;
+ };
+ };
+}
+
+interface ProjectsQueryResponse {
+ projects: {
+ edges: Array<{
+ node: {
+ id: string;
+ name: string;
+ description: string;
+ isPublic: boolean;
+ createdAt: string;
+ updatedAt: string;
+ teamId?: string;
+ environments: {
+ edges: Array<{
+ node: {
+ id: string;
+ name: string;
+ };
+ }>;
+ };
+ };
+ }>;
+ };
+}
+
+const PROJECT_FIELDS = `
+ id
+ name
+ description
+ isPublic
+ createdAt
+ updatedAt
+ teamId
+ environments {
+ edges {
+ node {
+ id
+ name
+ }
+ }
+ }
+`;
+
+/**
+ * Type guard for Railway Project
+ */
+export function isProject(resource: any): resource is Project {
+ return resource?.[ResourceKind] === "railway::Project";
+}
+
+/**
+ * Creates a Railway project for deploying services.
+ *
+ * @example
+ * // Create a basic Railway project:
+ * const project = await Project("my-project", {
+ * name: "My App"
+ * });
+ *
+ * @example
+ * // Adopt an existing Railway project by name:
+ * const project = await Project("my-project", {
+ * adopt: true,
+ * name: "existing-project-name",
+ * });
+ *
+ * @example
+ * // Create a project with description:
+ * const project = await Project("my-project", {
+ * name: "My App",
+ * description: "Production application",
+ * apiToken: alchemy.secret(process.env.RAILWAY_API_TOKEN)
+ * });
+ */
+export const Project = Resource(
+ "railway::Project",
+ async function (
+ this: Context,
+ id: string,
+ props: ProjectProps,
+ ): Promise {
+ const api = createRailwayApi(props);
+ const name =
+ props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);
+
+ if (this.phase === "delete") {
+ if (props.delete !== false && this.output?.id) {
+ try {
+ await api.request(
+ `
+ mutation projectDelete($id: String!) {
+ projectDelete(id: $id)
+ }
+ `,
+ { id: this.output.id },
+ );
+ } catch (error) {
+ // Ignore 404 errors (project already deleted)
+ if (error instanceof RailwayError && error.code === "NOT_FOUND") {
+ // Already deleted, that's fine
+ } else {
+ throw error;
+ }
+ }
+ }
+ return this.destroy();
+ }
+
+ // Handle adoption
+ if (this.phase === "create" && props.adopt) {
+ const existingProject = await findProjectByName(api, name, props.teamId);
+ if (existingProject) {
+ return existingProject;
+ }
+ // Project not found, continue with creation
+ }
+
+ // Update existing project
+ if (this.phase === "update" && this.output?.id) {
+ const data = await api.request(
+ `
+ mutation projectUpdate($id: String!, $input: ProjectUpdateInput!) {
+ projectUpdate(id: $id, input: $input) {
+ ${PROJECT_FIELDS}
+ }
+ }
+ `,
+ {
+ id: this.output.id,
+ input: {
+ name,
+ description: props.description ?? "",
+ isPublic: props.isPublic ?? false,
+ },
+ },
+ );
+
+ const project = data.projectUpdate;
+ return {
+ id: project.id,
+ name: project.name,
+ description: project.description,
+ isPublic: project.isPublic,
+ createdAt: project.createdAt,
+ updatedAt: project.updatedAt,
+ teamId: project.teamId,
+ environments: project.environments.edges.map((e) => ({
+ id: e.node.id,
+ name: e.node.name,
+ })),
+ };
+ }
+
+ // Create new project
+ const data = await api.request(
+ `
+ mutation projectCreate($input: ProjectCreateInput!) {
+ projectCreate(input: $input) {
+ ${PROJECT_FIELDS}
+ }
+ }
+ `,
+ {
+ input: {
+ name,
+ description: props.description ?? "",
+ isPublic: props.isPublic ?? false,
+ teamId: props.teamId,
+ },
+ },
+ );
+
+ const project = data.projectCreate;
+ return {
+ id: project.id,
+ name: project.name,
+ description: project.description,
+ isPublic: project.isPublic,
+ createdAt: project.createdAt,
+ updatedAt: project.updatedAt,
+ teamId: project.teamId,
+ environments: project.environments.edges.map((e) => ({
+ id: e.node.id,
+ name: e.node.name,
+ })),
+ };
+ },
+);
+
+/**
+ * Reference an existing Railway project by ID
+ */
+export async function ProjectRef(
+ props: RailwayApiOptions & { projectId: string },
+): Promise {
+ const api = createRailwayApi(props);
+
+ const data = await api.request(
+ `
+ query project($id: String!) {
+ project(id: $id) {
+ ${PROJECT_FIELDS}
+ }
+ }
+ `,
+ { id: props.projectId },
+ );
+
+ const project = data.project;
+ return {
+ id: project.id,
+ name: project.name,
+ description: project.description,
+ isPublic: project.isPublic,
+ createdAt: project.createdAt,
+ updatedAt: project.updatedAt,
+ teamId: project.teamId,
+ environments: project.environments.edges.map((e) => ({
+ id: e.node.id,
+ name: e.node.name,
+ })),
+ };
+}
+
+async function findProjectByName(
+ api: ReturnType,
+ name: string,
+ teamId?: string,
+): Promise {
+ const data = await api.request(
+ `
+ query projects($teamId: String) {
+ projects(teamId: $teamId) {
+ edges {
+ node {
+ ${PROJECT_FIELDS}
+ }
+ }
+ }
+ }
+ `,
+ { teamId },
+ );
+
+ const matchingProject = data.projects.edges.find(
+ (edge) => edge.node.name === name,
+ );
+
+ if (!matchingProject) {
+ return null;
+ }
+
+ const project = matchingProject.node;
+ return {
+ id: project.id,
+ name: project.name,
+ description: project.description,
+ isPublic: project.isPublic,
+ createdAt: project.createdAt,
+ updatedAt: project.updatedAt,
+ teamId: project.teamId,
+ environments: project.environments.edges.map((e) => ({
+ id: e.node.id,
+ name: e.node.name,
+ })),
+ };
+}
diff --git a/alchemy/src/railway/service.ts b/alchemy/src/railway/service.ts
new file mode 100644
index 0000000000..6602005ab6
--- /dev/null
+++ b/alchemy/src/railway/service.ts
@@ -0,0 +1,618 @@
+import type { Context } from "../context.ts";
+import { Resource, ResourceKind } from "../resource.ts";
+import {
+ createRailwayApi,
+ type RailwayApiOptions,
+ RailwayError,
+} from "./api.ts";
+import { isEnvironment, type Environment } from "./environment.ts";
+import { isProject, type Project } from "./project.ts";
+
+/**
+ * Source configuration for deploying from a GitHub repository
+ */
+export interface GitHubSource {
+ /**
+ * GitHub repository in format "owner/repo"
+ */
+ repo: string;
+
+ /**
+ * Branch to deploy from
+ * @default main
+ */
+ branch?: string;
+}
+
+/**
+ * Source configuration for deploying from a Docker image
+ */
+export interface DockerSource {
+ /**
+ * Docker image to deploy (e.g., "nginx:latest")
+ */
+ image: string;
+}
+
+/**
+ * Properties for creating or updating a Railway service
+ */
+export interface ServiceProps extends RailwayApiOptions {
+ /**
+ * The project to create the service in
+ */
+ project: string | Project;
+
+ /**
+ * The environment to deploy to (if not specified, deploys to all environments)
+ */
+ environment?: string | Environment;
+
+ /**
+ * Name of the service
+ *
+ * @default ${app}-${stage}-${id}
+ */
+ name?: string;
+
+ /**
+ * Source to deploy from - either a GitHub repo or Docker image
+ */
+ source?: GitHubSource | DockerSource;
+
+ /**
+ * Build command to run before starting the service
+ */
+ buildCommand?: string;
+
+ /**
+ * Command to start the service
+ */
+ startCommand?: string;
+
+ /**
+ * Root directory containing the application code (relative to repo root)
+ */
+ rootDirectory?: string;
+
+ /**
+ * Number of replicas to run
+ * @default 1
+ */
+ numReplicas?: number;
+
+ /**
+ * Health check path for HTTP services
+ */
+ healthcheckPath?: string;
+
+ /**
+ * Health check timeout in seconds
+ */
+ healthcheckTimeout?: number;
+
+ /**
+ * Whether to enable the restart policy
+ * @default true
+ */
+ restartPolicyType?: "ON_FAILURE" | "ALWAYS" | "NEVER";
+
+ /**
+ * Maximum number of restart retries
+ */
+ restartPolicyMaxRetries?: number;
+
+ /**
+ * When `true`, will adopt an existing service by `name`
+ */
+ adopt?: true;
+
+ /**
+ * Whether to delete the service when the resource is destroyed.
+ * @default true
+ */
+ delete?: boolean;
+}
+
+/**
+ * Domain associated with a service
+ */
+export interface ServiceDomain {
+ id: string;
+ domain: string;
+}
+
+/**
+ * Output returned after Railway service creation/update
+ */
+export interface Service {
+ /**
+ * The ID of the service
+ */
+ id: string;
+
+ /**
+ * Name of the service
+ */
+ name: string;
+
+ /**
+ * The project ID this service belongs to
+ */
+ projectId: string;
+
+ /**
+ * Time at which the service was created
+ */
+ createdAt: string;
+
+ /**
+ * Time at which the service was last updated
+ */
+ updatedAt: string;
+
+ /**
+ * Domains associated with the service
+ */
+ domains: ServiceDomain[];
+
+ /**
+ * Icon for the service (if set)
+ */
+ icon?: string;
+}
+
+/**
+ * GraphQL response types
+ * @internal
+ */
+interface ServiceCreateResponse {
+ serviceCreate: {
+ id: string;
+ name: string;
+ projectId: string;
+ createdAt: string;
+ updatedAt: string;
+ icon?: string;
+ };
+}
+
+interface ServiceUpdateResponse {
+ serviceUpdate: {
+ id: string;
+ name: string;
+ projectId: string;
+ createdAt: string;
+ updatedAt: string;
+ icon?: string;
+ };
+}
+
+interface ServiceQueryResponse {
+ service: {
+ id: string;
+ name: string;
+ projectId: string;
+ createdAt: string;
+ updatedAt: string;
+ icon?: string;
+ serviceDomains: {
+ id: string;
+ domain: string;
+ }[];
+ };
+}
+
+interface ServicesQueryResponse {
+ project: {
+ services: {
+ edges: Array<{
+ node: {
+ id: string;
+ name: string;
+ projectId: string;
+ createdAt: string;
+ updatedAt: string;
+ icon?: string;
+ serviceDomains: {
+ id: string;
+ domain: string;
+ }[];
+ };
+ }>;
+ };
+ };
+}
+
+const SERVICE_FIELDS = `
+ id
+ name
+ projectId
+ createdAt
+ updatedAt
+ icon
+`;
+
+/**
+ * Type guard for Railway Service
+ */
+export function isService(resource: any): resource is Service {
+ return resource?.[ResourceKind] === "railway::Service";
+}
+
+/**
+ * Get the project ID from a project reference
+ */
+function getProjectId(project: string | Project): string {
+ if (isProject(project)) {
+ return project.id;
+ }
+ return project;
+}
+
+/**
+ * Get the environment ID from an environment reference
+ */
+function getEnvironmentId(
+ environment: string | Environment | undefined,
+): string | undefined {
+ if (environment === undefined) {
+ return undefined;
+ }
+ if (isEnvironment(environment)) {
+ return environment.id;
+ }
+ return environment;
+}
+
+/**
+ * Check if source is a GitHub source
+ */
+function isGitHubSource(
+ source: GitHubSource | DockerSource | undefined,
+): source is GitHubSource {
+ return source !== undefined && "repo" in source;
+}
+
+/**
+ * Check if source is a Docker source
+ */
+function isDockerSource(
+ source: GitHubSource | DockerSource | undefined,
+): source is DockerSource {
+ return source !== undefined && "image" in source;
+}
+
+/**
+ * Creates a Railway service for deploying applications.
+ *
+ * @example
+ * // Deploy from a GitHub repository:
+ * const api = await Service("api", {
+ * project: project,
+ * source: {
+ * repo: "myorg/api",
+ * branch: "main"
+ * },
+ * startCommand: "npm start"
+ * });
+ *
+ * @example
+ * // Deploy from a Docker image:
+ * const nginx = await Service("nginx", {
+ * project: project,
+ * source: {
+ * image: "nginx:latest"
+ * }
+ * });
+ *
+ * @example
+ * // Deploy with build configuration:
+ * const app = await Service("app", {
+ * project: project,
+ * source: { repo: "myorg/app" },
+ * buildCommand: "npm run build",
+ * startCommand: "npm start",
+ * rootDirectory: "packages/api"
+ * });
+ */
+export const Service = Resource(
+ "railway::Service",
+ async function (
+ this: Context,
+ id: string,
+ props: ServiceProps,
+ ): Promise {
+ const api = createRailwayApi(props);
+ const projectId = getProjectId(props.project);
+ const environmentId = getEnvironmentId(props.environment);
+ const name =
+ props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);
+
+ if (this.phase === "delete") {
+ if (props.delete !== false && this.output?.id) {
+ try {
+ await api.request(
+ `
+ mutation serviceDelete($id: String!) {
+ serviceDelete(id: $id)
+ }
+ `,
+ { id: this.output.id },
+ );
+ } catch (error) {
+ // Ignore 404 errors (service already deleted)
+ if (error instanceof RailwayError && error.code === "NOT_FOUND") {
+ // Already deleted, that's fine
+ } else {
+ throw error;
+ }
+ }
+ }
+ return this.destroy();
+ }
+
+ // Handle adoption
+ if (this.phase === "create" && props.adopt) {
+ const existingService = await findServiceByName(api, projectId, name);
+ if (existingService) {
+ return existingService;
+ }
+ // Service not found, continue with creation
+ }
+
+ // Update existing service
+ if (this.phase === "update" && this.output?.id) {
+ // Update the service
+ const data = await api.request(
+ `
+ mutation serviceUpdate($id: String!, $input: ServiceUpdateInput!) {
+ serviceUpdate(id: $id, input: $input) {
+ ${SERVICE_FIELDS}
+ }
+ }
+ `,
+ {
+ id: this.output.id,
+ input: {
+ name,
+ },
+ },
+ );
+
+ // Update service instance if environment is specified
+ if (environmentId) {
+ await updateServiceInstance(api, this.output.id, environmentId, props);
+ }
+
+ // Fetch domains
+ const serviceData = await fetchService(api, this.output.id);
+
+ return {
+ id: data.serviceUpdate.id,
+ name: data.serviceUpdate.name,
+ projectId: data.serviceUpdate.projectId,
+ createdAt: data.serviceUpdate.createdAt,
+ updatedAt: data.serviceUpdate.updatedAt,
+ icon: data.serviceUpdate.icon,
+ domains: serviceData.domains,
+ };
+ }
+
+ // Build source input
+ let sourceInput: Record | undefined;
+ if (isGitHubSource(props.source)) {
+ sourceInput = {
+ repo: props.source.repo,
+ };
+ } else if (isDockerSource(props.source)) {
+ sourceInput = {
+ image: props.source.image,
+ };
+ }
+
+ // Create new service
+ const createInput: Record = {
+ name,
+ projectId,
+ };
+
+ if (sourceInput) {
+ createInput.source = sourceInput;
+ }
+
+ if (environmentId) {
+ createInput.environmentId = environmentId;
+ }
+
+ if (props.source && isGitHubSource(props.source) && props.source.branch) {
+ createInput.branch = props.source.branch;
+ }
+
+ const data = await api.request(
+ `
+ mutation serviceCreate($input: ServiceCreateInput!) {
+ serviceCreate(input: $input) {
+ ${SERVICE_FIELDS}
+ }
+ }
+ `,
+ { input: createInput },
+ );
+
+ // Update service instance with build/start commands if provided
+ if (
+ environmentId &&
+ (props.buildCommand || props.startCommand || props.rootDirectory)
+ ) {
+ await updateServiceInstance(
+ api,
+ data.serviceCreate.id,
+ environmentId,
+ props,
+ );
+ }
+
+ return {
+ id: data.serviceCreate.id,
+ name: data.serviceCreate.name,
+ projectId: data.serviceCreate.projectId,
+ createdAt: data.serviceCreate.createdAt,
+ updatedAt: data.serviceCreate.updatedAt,
+ icon: data.serviceCreate.icon,
+ domains: [],
+ };
+ },
+);
+
+/**
+ * Update service instance configuration
+ */
+async function updateServiceInstance(
+ api: ReturnType,
+ serviceId: string,
+ environmentId: string,
+ props: ServiceProps,
+): Promise {
+ const input: Record = {};
+
+ if (props.buildCommand !== undefined) {
+ input.buildCommand = props.buildCommand;
+ }
+ if (props.startCommand !== undefined) {
+ input.startCommand = props.startCommand;
+ }
+ if (props.rootDirectory !== undefined) {
+ input.rootDirectory = props.rootDirectory;
+ }
+ if (props.numReplicas !== undefined) {
+ input.numReplicas = props.numReplicas;
+ }
+ if (props.healthcheckPath !== undefined) {
+ input.healthcheckPath = props.healthcheckPath;
+ }
+ if (props.healthcheckTimeout !== undefined) {
+ input.healthcheckTimeout = props.healthcheckTimeout;
+ }
+ if (props.restartPolicyType !== undefined) {
+ input.restartPolicyType = props.restartPolicyType;
+ }
+ if (props.restartPolicyMaxRetries !== undefined) {
+ input.restartPolicyMaxRetries = props.restartPolicyMaxRetries;
+ }
+
+ if (Object.keys(input).length === 0) {
+ return;
+ }
+
+ await api.request(
+ `
+ mutation serviceInstanceUpdate($serviceId: String!, $environmentId: String!, $input: ServiceInstanceUpdateInput!) {
+ serviceInstanceUpdate(serviceId: $serviceId, environmentId: $environmentId, input: $input)
+ }
+ `,
+ {
+ serviceId,
+ environmentId,
+ input,
+ },
+ );
+}
+
+/**
+ * Fetch service with domains
+ */
+async function fetchService(
+ api: ReturnType,
+ serviceId: string,
+): Promise {
+ const data = await api.request(
+ `
+ query service($id: String!) {
+ service(id: $id) {
+ ${SERVICE_FIELDS}
+ serviceDomains {
+ id
+ domain
+ }
+ }
+ }
+ `,
+ { id: serviceId },
+ );
+
+ return {
+ id: data.service.id,
+ name: data.service.name,
+ projectId: data.service.projectId,
+ createdAt: data.service.createdAt,
+ updatedAt: data.service.updatedAt,
+ icon: data.service.icon,
+ domains: data.service.serviceDomains.map((d) => ({
+ id: d.id,
+ domain: d.domain,
+ })),
+ };
+}
+
+/**
+ * Find service by name in a project
+ */
+async function findServiceByName(
+ api: ReturnType,
+ projectId: string,
+ name: string,
+): Promise {
+ const data = await api.request(
+ `
+ query services($projectId: String!) {
+ project(id: $projectId) {
+ services {
+ edges {
+ node {
+ ${SERVICE_FIELDS}
+ serviceDomains {
+ id
+ domain
+ }
+ }
+ }
+ }
+ }
+ }
+ `,
+ { projectId },
+ );
+
+ const matchingService = data.project.services.edges.find(
+ (edge) => edge.node.name === name,
+ );
+
+ if (!matchingService) {
+ return null;
+ }
+
+ const service = matchingService.node;
+ return {
+ id: service.id,
+ name: service.name,
+ projectId: service.projectId,
+ createdAt: service.createdAt,
+ updatedAt: service.updatedAt,
+ icon: service.icon,
+ domains: service.serviceDomains.map((d) => ({
+ id: d.id,
+ domain: d.domain,
+ })),
+ };
+}
+
+/**
+ * Reference an existing Railway service by ID
+ */
+export async function ServiceRef(
+ props: RailwayApiOptions & { serviceId: string },
+): Promise {
+ const api = createRailwayApi(props);
+ return fetchService(api, props.serviceId);
+}
diff --git a/alchemy/src/railway/tcp-proxy.ts b/alchemy/src/railway/tcp-proxy.ts
new file mode 100644
index 0000000000..fdbafbd2c7
--- /dev/null
+++ b/alchemy/src/railway/tcp-proxy.ts
@@ -0,0 +1,204 @@
+import type { Context } from "../context.ts";
+import { Resource, ResourceKind } from "../resource.ts";
+import {
+ createRailwayApi,
+ type RailwayApiOptions,
+ RailwayError,
+} from "./api.ts";
+import { isEnvironment, type Environment } from "./environment.ts";
+import { isService, type Service } from "./service.ts";
+
+/**
+ * Properties for creating a Railway TCP proxy
+ */
+export interface TcpProxyProps extends RailwayApiOptions {
+ /**
+ * The service to attach the TCP proxy to
+ */
+ service: string | Service;
+
+ /**
+ * The environment to create the TCP proxy in
+ */
+ environment: string | Environment;
+
+ /**
+ * The application port to proxy traffic to
+ */
+ applicationPort: number;
+
+ /**
+ * Whether to delete the TCP proxy when the resource is destroyed.
+ * @default true
+ */
+ delete?: boolean;
+}
+
+/**
+ * Output returned after Railway TCP proxy creation
+ */
+export interface TcpProxy {
+ /**
+ * The ID of the TCP proxy
+ */
+ id: string;
+
+ /**
+ * The domain to connect to
+ */
+ domain: string;
+
+ /**
+ * The proxy port to connect to
+ */
+ proxyPort: number;
+
+ /**
+ * The application port traffic is forwarded to
+ */
+ applicationPort: number;
+
+ /**
+ * The service ID this TCP proxy is attached to
+ */
+ serviceId: string;
+
+ /**
+ * The environment ID this TCP proxy belongs to
+ */
+ environmentId: string;
+}
+
+/**
+ * GraphQL response types
+ * @internal
+ */
+interface TcpProxyCreateResponse {
+ tcpProxyCreate: {
+ id: string;
+ domain: string;
+ proxyPort: number;
+ applicationPort: number;
+ };
+}
+
+/**
+ * Type guard for Railway TcpProxy
+ */
+export function isTcpProxy(resource: any): resource is TcpProxy {
+ return resource?.[ResourceKind] === "railway::TcpProxy";
+}
+
+/**
+ * Get the service ID from a service reference
+ */
+function getServiceId(service: string | Service): string {
+ if (isService(service)) {
+ return service.id;
+ }
+ return service;
+}
+
+/**
+ * Get the environment ID from an environment reference
+ */
+function getEnvironmentId(environment: string | Environment): string {
+ if (isEnvironment(environment)) {
+ return environment.id;
+ }
+ return environment;
+}
+
+/**
+ * Creates a Railway TCP proxy for non-HTTP traffic.
+ *
+ * @example
+ * // Create a TCP proxy for a database:
+ * const proxy = await TcpProxy("db-proxy", {
+ * service: postgresService,
+ * environment: production,
+ * applicationPort: 5432
+ * });
+ * console.log(`Connect to: ${proxy.domain}:${proxy.proxyPort}`);
+ *
+ * @example
+ * // Create a TCP proxy for Redis:
+ * const redisProxy = await TcpProxy("redis-proxy", {
+ * service: redisService,
+ * environment: production,
+ * applicationPort: 6379
+ * });
+ */
+export const TcpProxy = Resource(
+ "railway::TcpProxy",
+ async function (
+ this: Context,
+ id: string,
+ props: TcpProxyProps,
+ ): Promise {
+ const api = createRailwayApi(props);
+ const serviceId = getServiceId(props.service);
+ const environmentId = getEnvironmentId(props.environment);
+
+ if (this.phase === "delete") {
+ if (props.delete !== false && this.output?.id) {
+ try {
+ await api.request(
+ `
+ mutation tcpProxyDelete($id: String!) {
+ tcpProxyDelete(id: $id)
+ }
+ `,
+ { id: this.output.id },
+ );
+ } catch (error) {
+ // Ignore 404 errors (TCP proxy already deleted)
+ if (error instanceof RailwayError && error.code === "NOT_FOUND") {
+ // Already deleted, that's fine
+ } else {
+ throw error;
+ }
+ }
+ }
+ return this.destroy();
+ }
+
+ // TCP proxies are immutable - port changes require replacement
+ if (this.phase === "update" && this.output?.id) {
+ if (this.output.applicationPort !== props.applicationPort) {
+ return this.replace();
+ }
+ return this.output;
+ }
+
+ // Create new TCP proxy
+ const data = await api.request(
+ `
+ mutation tcpProxyCreate($input: TCPProxyCreateInput!) {
+ tcpProxyCreate(input: $input) {
+ id
+ domain
+ proxyPort
+ applicationPort
+ }
+ }
+ `,
+ {
+ input: {
+ serviceId,
+ environmentId,
+ applicationPort: props.applicationPort,
+ },
+ },
+ );
+
+ return {
+ id: data.tcpProxyCreate.id,
+ domain: data.tcpProxyCreate.domain,
+ proxyPort: data.tcpProxyCreate.proxyPort,
+ applicationPort: data.tcpProxyCreate.applicationPort,
+ serviceId,
+ environmentId,
+ };
+ },
+);
diff --git a/alchemy/src/railway/variable.ts b/alchemy/src/railway/variable.ts
new file mode 100644
index 0000000000..1ff9fc671b
--- /dev/null
+++ b/alchemy/src/railway/variable.ts
@@ -0,0 +1,262 @@
+import type { Context } from "../context.ts";
+import { Resource, ResourceKind } from "../resource.ts";
+import { Secret } from "../secret.ts";
+import {
+ createRailwayApi,
+ type RailwayApiOptions,
+ RailwayError,
+} from "./api.ts";
+import { isEnvironment, type Environment } from "./environment.ts";
+import { isProject, type Project } from "./project.ts";
+import { isService, type Service } from "./service.ts";
+
+/**
+ * Properties for creating or updating a Railway variable
+ */
+export interface VariableProps extends RailwayApiOptions {
+ /**
+ * The project to create the variable in
+ */
+ project: string | Project;
+
+ /**
+ * The environment to create the variable in
+ */
+ environment: string | Environment;
+
+ /**
+ * The service to attach the variable to (optional for shared variables)
+ */
+ service?: string | Service;
+
+ /**
+ * Name of the variable
+ */
+ name: string;
+
+ /**
+ * Value of the variable
+ */
+ value: string | Secret;
+
+ /**
+ * Whether to delete the variable when the resource is destroyed.
+ * @default true
+ */
+ delete?: boolean;
+}
+
+/**
+ * Output returned after Railway variable creation/update
+ */
+export interface Variable {
+ /**
+ * The name of the variable
+ */
+ name: string;
+
+ /**
+ * The value of the variable (wrapped as Secret)
+ */
+ value: Secret;
+
+ /**
+ * The project ID this variable belongs to
+ */
+ projectId: string;
+
+ /**
+ * The environment ID this variable belongs to
+ */
+ environmentId: string;
+
+ /**
+ * The service ID this variable is attached to (if service-specific)
+ */
+ serviceId?: string;
+}
+
+/**
+ * GraphQL response types
+ * @internal
+ */
+interface VariableUpsertResponse {
+ variableUpsert: boolean;
+}
+
+interface VariablesQueryResponse {
+ variables: Record;
+}
+
+/**
+ * Type guard for Railway Variable
+ */
+export function isVariable(resource: any): resource is Variable {
+ return resource?.[ResourceKind] === "railway::Variable";
+}
+
+/**
+ * Get the project ID from a project reference
+ */
+function getProjectId(project: string | Project): string {
+ if (isProject(project)) {
+ return project.id;
+ }
+ return project;
+}
+
+/**
+ * Get the environment ID from an environment reference
+ */
+function getEnvironmentId(environment: string | Environment): string {
+ if (isEnvironment(environment)) {
+ return environment.id;
+ }
+ return environment;
+}
+
+/**
+ * Get the service ID from a service reference
+ */
+function getServiceId(
+ service: string | Service | undefined,
+): string | undefined {
+ if (service === undefined) {
+ return undefined;
+ }
+ if (isService(service)) {
+ return service.id;
+ }
+ return service;
+}
+
+/**
+ * Creates a Railway environment variable.
+ *
+ * @example
+ * // Create a service-specific variable:
+ * const dbUrl = await Variable("db-url", {
+ * project: project,
+ * environment: production,
+ * service: apiService,
+ * name: "DATABASE_URL",
+ * value: database.connectionUrl
+ * });
+ *
+ * @example
+ * // Create a shared variable (available to all services):
+ * const apiKey = await Variable("api-key", {
+ * project: project,
+ * environment: production,
+ * name: "API_KEY",
+ * value: alchemy.secret(process.env.API_KEY)
+ * });
+ *
+ * @example
+ * // Use Railway reference syntax:
+ * const redisUrl = await Variable("redis-url", {
+ * project: project,
+ * environment: production,
+ * service: apiService,
+ * name: "REDIS_URL",
+ * value: "${{Redis.REDIS_URL}}"
+ * });
+ */
+export const Variable = Resource(
+ "railway::Variable",
+ async function (
+ this: Context,
+ id: string,
+ props: VariableProps,
+ ): Promise {
+ const api = createRailwayApi(props);
+ const projectId = getProjectId(props.project);
+ const environmentId = getEnvironmentId(props.environment);
+ const serviceId = getServiceId(props.service);
+ const value = Secret.unwrap(props.value);
+
+ if (this.phase === "delete") {
+ if (props.delete !== false) {
+ try {
+ await api.request(
+ `
+ mutation variableDelete($input: VariableDeleteInput!) {
+ variableDelete(input: $input)
+ }
+ `,
+ {
+ input: {
+ projectId,
+ environmentId,
+ serviceId,
+ name: props.name,
+ },
+ },
+ );
+ } catch (error) {
+ // Ignore 404 errors (variable already deleted)
+ if (error instanceof RailwayError && error.code === "NOT_FOUND") {
+ // Already deleted, that's fine
+ } else {
+ throw error;
+ }
+ }
+ }
+ return this.destroy();
+ }
+
+ // Upsert the variable (works for both create and update)
+ await api.request(
+ `
+ mutation variableUpsert($input: VariableUpsertInput!) {
+ variableUpsert(input: $input)
+ }
+ `,
+ {
+ input: {
+ projectId,
+ environmentId,
+ serviceId,
+ name: props.name,
+ value,
+ },
+ },
+ );
+
+ return {
+ name: props.name,
+ value: Secret.wrap(props.value),
+ projectId,
+ environmentId,
+ serviceId,
+ };
+ },
+);
+
+/**
+ * Fetch all variables for a project/environment/service
+ */
+export async function getVariables(
+ props: RailwayApiOptions & {
+ projectId: string;
+ environmentId: string;
+ serviceId?: string;
+ },
+): Promise> {
+ const api = createRailwayApi(props);
+
+ const data = await api.request(
+ `
+ query variables($projectId: String!, $environmentId: String!, $serviceId: String) {
+ variables(projectId: $projectId, environmentId: $environmentId, serviceId: $serviceId)
+ }
+ `,
+ {
+ projectId: props.projectId,
+ environmentId: props.environmentId,
+ serviceId: props.serviceId,
+ },
+ );
+
+ return data.variables;
+}
diff --git a/alchemy/src/railway/volume.ts b/alchemy/src/railway/volume.ts
new file mode 100644
index 0000000000..51f2b56d90
--- /dev/null
+++ b/alchemy/src/railway/volume.ts
@@ -0,0 +1,312 @@
+import type { Context } from "../context.ts";
+import { Resource, ResourceKind } from "../resource.ts";
+import {
+ createRailwayApi,
+ type RailwayApiOptions,
+ RailwayError,
+} from "./api.ts";
+import { isEnvironment, type Environment } from "./environment.ts";
+import { isProject, type Project } from "./project.ts";
+import { isService, type Service } from "./service.ts";
+
+/**
+ * Properties for creating a Railway volume
+ */
+export interface VolumeProps extends RailwayApiOptions {
+ /**
+ * The project to create the volume in
+ */
+ project: string | Project;
+
+ /**
+ * The service to attach the volume to
+ */
+ service: string | Service;
+
+ /**
+ * The environment to create the volume in
+ */
+ environment: string | Environment;
+
+ /**
+ * Name of the volume
+ *
+ * @default ${app}-${stage}-${id}
+ */
+ name?: string;
+
+ /**
+ * Mount path inside the container
+ */
+ mountPath: string;
+
+ /**
+ * Whether to delete the volume when the resource is destroyed.
+ * Note: Deleting a volume will permanently delete all data.
+ * @default true
+ */
+ delete?: boolean;
+}
+
+/**
+ * Output returned after Railway volume creation
+ */
+export interface Volume {
+ /**
+ * The ID of the volume
+ */
+ id: string;
+
+ /**
+ * Name of the volume
+ */
+ name: string;
+
+ /**
+ * Mount path inside the container
+ */
+ mountPath: string;
+
+ /**
+ * The project ID this volume belongs to
+ */
+ projectId: string;
+
+ /**
+ * The service ID this volume is attached to
+ */
+ serviceId: string;
+
+ /**
+ * The environment ID this volume belongs to
+ */
+ environmentId: string;
+
+ /**
+ * Time at which the volume was created
+ */
+ createdAt: string;
+}
+
+/**
+ * GraphQL response types
+ * @internal
+ */
+interface VolumeCreateResponse {
+ volumeCreate: {
+ id: string;
+ name: string;
+ mountPath: string;
+ projectId: string;
+ createdAt: string;
+ };
+}
+
+interface VolumeUpdateResponse {
+ volumeUpdate: {
+ id: string;
+ name: string;
+ mountPath: string;
+ projectId: string;
+ createdAt: string;
+ };
+}
+
+const VOLUME_FIELDS = `
+ id
+ name
+ mountPath
+ projectId
+ createdAt
+`;
+
+/**
+ * Type guard for Railway Volume
+ */
+export function isVolume(resource: any): resource is Volume {
+ return resource?.[ResourceKind] === "railway::Volume";
+}
+
+/**
+ * Get the project ID from a project reference
+ */
+function getProjectId(project: string | Project): string {
+ if (isProject(project)) {
+ return project.id;
+ }
+ return project;
+}
+
+/**
+ * Get the service ID from a service reference
+ */
+function getServiceId(service: string | Service): string {
+ if (isService(service)) {
+ return service.id;
+ }
+ return service;
+}
+
+/**
+ * Get the environment ID from an environment reference
+ */
+function getEnvironmentId(environment: string | Environment): string {
+ if (isEnvironment(environment)) {
+ return environment.id;
+ }
+ return environment;
+}
+
+/**
+ * Creates a Railway volume for persistent storage.
+ *
+ * @example
+ * // Create a volume for database storage:
+ * const dataVolume = await Volume("data", {
+ * project: project,
+ * service: dbService,
+ * environment: production,
+ * name: "postgres-data",
+ * mountPath: "/var/lib/postgresql/data"
+ * });
+ *
+ * @example
+ * // Create a volume for file uploads:
+ * const uploadsVolume = await Volume("uploads", {
+ * project: project,
+ * service: apiService,
+ * environment: production,
+ * mountPath: "/app/uploads"
+ * });
+ */
+export const Volume = Resource(
+ "railway::Volume",
+ async function (
+ this: Context,
+ id: string,
+ props: VolumeProps,
+ ): Promise {
+ const api = createRailwayApi(props);
+ const projectId = getProjectId(props.project);
+ const serviceId = getServiceId(props.service);
+ const environmentId = getEnvironmentId(props.environment);
+ const name =
+ props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);
+
+ if (this.phase === "delete") {
+ if (props.delete !== false && this.output?.id) {
+ try {
+ await api.request(
+ `
+ mutation volumeDelete($id: String!) {
+ volumeDelete(id: $id)
+ }
+ `,
+ { id: this.output.id },
+ );
+ } catch (error) {
+ // Ignore 404 errors (volume already deleted)
+ if (error instanceof RailwayError && error.code === "NOT_FOUND") {
+ // Already deleted, that's fine
+ } else {
+ throw error;
+ }
+ }
+ }
+ return this.destroy();
+ }
+
+ // Update existing volume
+ if (this.phase === "update" && this.output?.id) {
+ // Mount path change requires replacement
+ if (this.output.mountPath !== props.mountPath) {
+ return this.replace();
+ }
+
+ // Update name if changed
+ if (this.output.name !== name) {
+ const data = await api.request(
+ `
+ mutation volumeUpdate($id: String!, $input: VolumeUpdateInput!) {
+ volumeUpdate(id: $id, input: $input) {
+ ${VOLUME_FIELDS}
+ }
+ }
+ `,
+ {
+ id: this.output.id,
+ input: { name },
+ },
+ );
+
+ return {
+ id: data.volumeUpdate.id,
+ name: data.volumeUpdate.name,
+ mountPath: data.volumeUpdate.mountPath,
+ projectId: data.volumeUpdate.projectId,
+ serviceId,
+ environmentId,
+ createdAt: data.volumeUpdate.createdAt,
+ };
+ }
+
+ return this.output;
+ }
+
+ // Create new volume
+ const data = await api.request(
+ `
+ mutation volumeCreate($input: VolumeCreateInput!) {
+ volumeCreate(input: $input) {
+ ${VOLUME_FIELDS}
+ }
+ }
+ `,
+ {
+ input: {
+ projectId,
+ serviceId,
+ environmentId,
+ mountPath: props.mountPath,
+ },
+ },
+ );
+
+ // Update volume name if different from default
+ if (name !== data.volumeCreate.name) {
+ const updateData = await api.request(
+ `
+ mutation volumeUpdate($id: String!, $input: VolumeUpdateInput!) {
+ volumeUpdate(id: $id, input: $input) {
+ ${VOLUME_FIELDS}
+ }
+ }
+ `,
+ {
+ id: data.volumeCreate.id,
+ input: { name },
+ },
+ );
+
+ return {
+ id: updateData.volumeUpdate.id,
+ name: updateData.volumeUpdate.name,
+ mountPath: updateData.volumeUpdate.mountPath,
+ projectId: updateData.volumeUpdate.projectId,
+ serviceId,
+ environmentId,
+ createdAt: updateData.volumeUpdate.createdAt,
+ };
+ }
+
+ return {
+ id: data.volumeCreate.id,
+ name: data.volumeCreate.name,
+ mountPath: data.volumeCreate.mountPath,
+ projectId: data.volumeCreate.projectId,
+ serviceId,
+ environmentId,
+ createdAt: data.volumeCreate.createdAt,
+ };
+ },
+);
diff --git a/alchemy/test/railway/project.test.ts b/alchemy/test/railway/project.test.ts
new file mode 100644
index 0000000000..43a22da0c8
--- /dev/null
+++ b/alchemy/test/railway/project.test.ts
@@ -0,0 +1,214 @@
+import { describe, expect } from "vitest";
+import { alchemy } from "../../src/alchemy.ts";
+import { destroy } from "../../src/destroy.ts";
+import { createRailwayApi } from "../../src/railway/api.ts";
+import { Project, ProjectRef } from "../../src/railway/index.ts";
+import { BRANCH_PREFIX } from "../util.ts";
+// must import this or else alchemy.test won't exist
+import "../../src/test/vitest.ts";
+
+// Create API client for verification
+const api = createRailwayApi();
+
+const test = alchemy.test(import.meta, {
+ prefix: BRANCH_PREFIX,
+});
+
+describe("Railway Project Resource", () => {
+ const testId = `${BRANCH_PREFIX}-test-railway-project`;
+
+ test("create, update, and delete project", async (scope) => {
+ let project: typeof Project.prototype | undefined;
+ try {
+ // Create a test Railway project
+ project = await Project(testId, {
+ name: `${testId}-test`,
+ description: "Test project for Alchemy",
+ });
+
+ expect(project.id).toBeTruthy();
+ expect(project.name).toEqual(`${testId}-test`);
+ expect(project.description).toEqual("Test project for Alchemy");
+ expect(project.createdAt).toBeTruthy();
+ expect(project.updatedAt).toBeTruthy();
+ expect(project.environments).toBeDefined();
+
+ // Verify project was created by querying the API directly
+ const data = await api.request<{
+ project: {
+ id: string;
+ name: string;
+ description: string;
+ };
+ }>(
+ `
+ query project($id: String!) {
+ project(id: $id) {
+ id
+ name
+ description
+ }
+ }
+ `,
+ { id: project.id },
+ );
+
+ expect(data.project.name).toEqual(`${testId}-test`);
+
+ // Update the project
+ const updatedDescription = "Updated test project";
+ project = await Project(testId, {
+ name: `${testId}-test`,
+ description: updatedDescription,
+ });
+
+ expect(project.description).toEqual(updatedDescription);
+
+ // Verify project was updated
+ const updatedData = await api.request<{
+ project: {
+ id: string;
+ name: string;
+ description: string;
+ };
+ }>(
+ `
+ query project($id: String!) {
+ project(id: $id) {
+ id
+ name
+ description
+ }
+ }
+ `,
+ { id: project.id },
+ );
+ expect(updatedData.project.description).toEqual(updatedDescription);
+ } finally {
+ // Always clean up
+ await destroy(scope);
+
+ // Verify project was deleted
+ if (project?.id) {
+ try {
+ await api.request(
+ `
+ query project($id: String!) {
+ project(id: $id) {
+ id
+ }
+ }
+ `,
+ { id: project.id },
+ );
+ // If we get here, project still exists - fail
+ expect(true).toBe(false);
+ } catch (error) {
+ // Project should not exist
+ expect(error).toBeDefined();
+ }
+ }
+ }
+ });
+
+ test("adopt existing project", async (scope) => {
+ let existingProjectId: string | undefined;
+ let adoptedProject: typeof Project.prototype | undefined;
+
+ try {
+ // Create a project via API first
+ const createData = await api.request<{
+ projectCreate: {
+ id: string;
+ name: string;
+ };
+ }>(
+ `
+ mutation projectCreate($input: ProjectCreateInput!) {
+ projectCreate(input: $input) {
+ id
+ name
+ }
+ }
+ `,
+ {
+ input: {
+ name: `${testId}-existing`,
+ description: "Existing project to adopt",
+ },
+ },
+ );
+
+ existingProjectId = createData.projectCreate.id;
+
+ // Adopt the project
+ adoptedProject = await Project(`${testId}-adopted`, {
+ adopt: true,
+ name: `${testId}-existing`,
+ });
+
+ expect(adoptedProject.id).toEqual(existingProjectId);
+ expect(adoptedProject.name).toEqual(`${testId}-existing`);
+ } finally {
+ await destroy(scope);
+
+ // Clean up the adopted project if it still exists
+ if (existingProjectId) {
+ try {
+ await api.request(
+ `
+ mutation projectDelete($id: String!) {
+ projectDelete(id: $id)
+ }
+ `,
+ { id: existingProjectId },
+ );
+ } catch {
+ // Ignore errors - project may have been deleted
+ }
+ }
+ }
+ });
+
+ test("does not delete project when delete is false", async (scope) => {
+ let project: typeof Project.prototype | undefined;
+ try {
+ project = await Project(`${testId}-no-delete`, {
+ name: `${testId}-no-delete`,
+ delete: false,
+ });
+ expect(project.id).toBeTruthy();
+ } finally {
+ await destroy(scope);
+
+ // Verify project still exists
+ if (project?.id) {
+ const data = await api.request<{
+ project: {
+ id: string;
+ };
+ }>(
+ `
+ query project($id: String!) {
+ project(id: $id) {
+ id
+ }
+ }
+ `,
+ { id: project.id },
+ );
+ expect(data.project.id).toEqual(project.id);
+
+ // Clean up manually
+ await api.request(
+ `
+ mutation projectDelete($id: String!) {
+ projectDelete(id: $id)
+ }
+ `,
+ { id: project.id },
+ );
+ }
+ }
+ });
+});
diff --git a/alchemy/test/railway/service.test.ts b/alchemy/test/railway/service.test.ts
new file mode 100644
index 0000000000..4943214e32
--- /dev/null
+++ b/alchemy/test/railway/service.test.ts
@@ -0,0 +1,132 @@
+import { describe, expect } from "vitest";
+import { alchemy } from "../../src/alchemy.ts";
+import { destroy } from "../../src/destroy.ts";
+import { createRailwayApi } from "../../src/railway/api.ts";
+import { Project, Service } from "../../src/railway/index.ts";
+import { BRANCH_PREFIX } from "../util.ts";
+import "../../src/test/vitest.ts";
+
+const api = createRailwayApi();
+
+const test = alchemy.test(import.meta, {
+ prefix: BRANCH_PREFIX,
+});
+
+describe("Railway Service Resource", () => {
+ const testId = `${BRANCH_PREFIX}-test-railway-service`;
+
+ test("create service from Docker image", async (scope) => {
+ let project: typeof Project.prototype | undefined;
+ let service: typeof Service.prototype | undefined;
+
+ try {
+ // Create a project first
+ project = await Project(`${testId}-project`, {
+ name: `${testId}-docker-project`,
+ });
+
+ // Create a service from Docker image
+ service = await Service(`${testId}-service`, {
+ project,
+ name: `${testId}-nginx`,
+ source: {
+ image: "nginx:alpine",
+ },
+ });
+
+ expect(service.id).toBeTruthy();
+ expect(service.name).toEqual(`${testId}-nginx`);
+ expect(service.projectId).toEqual(project.id);
+ expect(service.createdAt).toBeTruthy();
+
+ // Verify service was created via API
+ const data = await api.request<{
+ service: {
+ id: string;
+ name: string;
+ projectId: string;
+ };
+ }>(
+ `
+ query service($id: String!) {
+ service(id: $id) {
+ id
+ name
+ projectId
+ }
+ }
+ `,
+ { id: service.id },
+ );
+
+ expect(data.service.name).toEqual(`${testId}-nginx`);
+ expect(data.service.projectId).toEqual(project.id);
+ } finally {
+ await destroy(scope);
+ }
+ });
+
+ test("create service from GitHub repo", async (scope) => {
+ let project: typeof Project.prototype | undefined;
+ let service: typeof Service.prototype | undefined;
+
+ try {
+ project = await Project(`${testId}-gh-project`, {
+ name: `${testId}-github-project`,
+ });
+
+ // Create a service from a public GitHub repo
+ service = await Service(`${testId}-gh-service`, {
+ project,
+ name: `${testId}-from-github`,
+ source: {
+ repo: "railwayapp-templates/nextjs",
+ branch: "main",
+ },
+ });
+
+ expect(service.id).toBeTruthy();
+ expect(service.name).toEqual(`${testId}-from-github`);
+ expect(service.projectId).toEqual(project.id);
+ } finally {
+ await destroy(scope);
+ }
+ });
+
+ test("update service", async (scope) => {
+ let project: typeof Project.prototype | undefined;
+ let service: typeof Service.prototype | undefined;
+
+ try {
+ project = await Project(`${testId}-update-project`, {
+ name: `${testId}-update-test-project`,
+ });
+
+ // Create service
+ service = await Service(`${testId}-update-service`, {
+ project,
+ name: `${testId}-to-update`,
+ source: {
+ image: "nginx:alpine",
+ },
+ });
+
+ const originalId = service.id;
+
+ // Update service name
+ service = await Service(`${testId}-update-service`, {
+ project,
+ name: `${testId}-updated`,
+ source: {
+ image: "nginx:alpine",
+ },
+ });
+
+ // Service ID should remain the same
+ expect(service.id).toEqual(originalId);
+ expect(service.name).toEqual(`${testId}-updated`);
+ } finally {
+ await destroy(scope);
+ }
+ });
+});
diff --git a/bun.lock b/bun.lock
index 0f9343291a..707b677d05 100644
--- a/bun.lock
+++ b/bun.lock
@@ -796,6 +796,15 @@
"typescript": "catalog:",
},
},
+ "examples/railway": {
+ "name": "railway-example",
+ "version": "0.0.0",
+ "devDependencies": {
+ "@types/node": "^24.0.1",
+ "alchemy": "workspace:*",
+ "typescript": "catalog:",
+ },
+ },
"stacks": {
"name": "alchemy-stacks",
"version": "0.0.1",
@@ -4619,6 +4628,8 @@
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
+ "railway-example": ["railway-example@workspace:examples/railway"],
+
"randomatic": ["randomatic@3.1.1", "", { "dependencies": { "is-number": "^4.0.0", "kind-of": "^6.0.0", "math-random": "^1.0.1" } }, "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw=="],
"randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="],
@@ -6423,6 +6434,8 @@
"puppeteer-core/devtools-protocol": ["devtools-protocol@0.0.1312386", "", {}, "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA=="],
+ "railway-example/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
+
"randomatic/is-number": ["is-number@4.0.0", "", {}, "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ=="],
"randomatic/kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
@@ -7427,6 +7440,8 @@
"puppeteer-core/@puppeteer/browsers/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
+ "railway-example/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
+
"react-server-dom-webpack/react-dom/scheduler": ["scheduler@0.27.0-canary-39cad7af-20250411", "", {}, "sha512-u+r6WyKk4zmirtBMZP8QZx0sTptiRxMMbGjf/3nOunE0mNnmkGFDcsG2UVDuOb5CyxvfbxgWE6W5JIyzTN4FXQ=="],
"readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
diff --git a/examples/railway/README.md b/examples/railway/README.md
new file mode 100644
index 0000000000..0631fe7be1
--- /dev/null
+++ b/examples/railway/README.md
@@ -0,0 +1,76 @@
+# Railway Example
+
+This example demonstrates deploying a web service with a PostgreSQL database to Railway using Alchemy.
+
+## Prerequisites
+
+1. Create a Railway account at [railway.app](https://railway.app)
+2. Get an API token from [Railway account settings](https://railway.com/account/tokens)
+
+## Setup
+
+1. Add your Railway API token to `.env`:
+
+```bash
+RAILWAY_API_TOKEN=your_token_here
+```
+
+2. Install dependencies:
+
+```bash
+bun install
+```
+
+## Deploy
+
+```bash
+bun run deploy
+```
+
+This will:
+- Create a Railway project
+- Deploy a PostgreSQL database
+- Deploy an nginx web server
+- Configure the database connection as an environment variable
+- Create a public domain for the web service
+
+## Resources Created
+
+- **Project**: Container for all services
+- **PostgresDatabase**: Managed PostgreSQL database
+- **Service**: nginx web server (can be replaced with your app)
+- **Variable**: DATABASE_URL environment variable
+- **Domain**: Public URL for the web service
+
+## Tear Down
+
+```bash
+bun run destroy
+```
+
+⚠️ This will delete the database and all its data.
+
+## Customization
+
+Replace the nginx image with your own Docker image or GitHub repository:
+
+```typescript
+// From Docker image
+const api = await Service("api", {
+ project,
+ source: {
+ image: "your-registry/your-image:tag",
+ },
+});
+
+// From GitHub
+const api = await Service("api", {
+ project,
+ source: {
+ repo: "your-username/your-repo",
+ branch: "main",
+ },
+ buildCommand: "npm run build",
+ startCommand: "npm start",
+});
+```
diff --git a/examples/railway/alchemy.run.ts b/examples/railway/alchemy.run.ts
new file mode 100644
index 0000000000..66d7d40e8e
--- /dev/null
+++ b/examples/railway/alchemy.run.ts
@@ -0,0 +1,62 @@
+import alchemy from "alchemy";
+import {
+ Project,
+ Service,
+ PostgresDatabase,
+ Domain,
+ Variable,
+} from "alchemy/railway";
+
+const app = await alchemy("railway-example");
+
+// Create a Railway project
+const project = await Project("my-project", {
+ name: `${app.name}-${app.stage}`,
+ description: "Railway example with Alchemy",
+});
+
+console.log(`Created project: ${project.name} (${project.id})`);
+
+// Deploy a PostgreSQL database
+const db = await PostgresDatabase("db", {
+ project,
+ name: `${app.name}-postgres`,
+});
+
+console.log(`Created database: ${db.name} (${db.id})`);
+console.log(`Database host: ${db.host}`);
+
+// Deploy an nginx service as a simple web server
+const web = await Service("web", {
+ project,
+ name: `${app.name}-web`,
+ source: {
+ image: "nginx:alpine",
+ },
+});
+
+console.log(`Created service: ${web.name} (${web.id})`);
+
+// Get the production environment
+const productionEnv = project.environments[0];
+
+if (productionEnv) {
+ // Add database URL as environment variable
+ await Variable("db-url", {
+ project,
+ environment: productionEnv.id,
+ service: web,
+ name: "DATABASE_URL",
+ value: db.connectionUrl,
+ });
+
+ // Create a public domain for the web service
+ const domain = await Domain("web-domain", {
+ service: web,
+ environment: productionEnv.id,
+ });
+
+ console.log(`Web service available at: https://${domain.domain}`);
+}
+
+await app.finalize();
diff --git a/examples/railway/package.json b/examples/railway/package.json
new file mode 100644
index 0000000000..0fdf720f82
--- /dev/null
+++ b/examples/railway/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "railway-example",
+ "version": "0.0.0",
+ "description": "Deploy a service with PostgreSQL to Railway using Alchemy",
+ "type": "module",
+ "scripts": {
+ "build": "tsc -b",
+ "deploy": "alchemy deploy --env-file ../../.env",
+ "destroy": "alchemy destroy --env-file ../../.env"
+ },
+ "devDependencies": {
+ "@types/node": "^24.0.1",
+ "alchemy": "workspace:*",
+ "typescript": "catalog:"
+ }
+}
diff --git a/examples/railway/tsconfig.json b/examples/railway/tsconfig.json
new file mode 100644
index 0000000000..5f755f38f8
--- /dev/null
+++ b/examples/railway/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "allowImportingTsExtensions": true,
+ "rewriteRelativeImportExtensions": true,
+ "noEmit": true
+ },
+ "include": ["alchemy.run.ts"],
+ "references": [
+ {
+ "path": "../../alchemy/tsconfig.json"
+ }
+ ]
+}