feat(executors): add Railway executor template and documentation

- Add Railway executor template with one-click deploy support
- Create Railway documentation page at /docs/executors/railway
- Update main executors page with Railway as official recommendation
- Add Railway to platform comparison table with new columns
- Update inter-page navigation for Railway → Unsandbox → Vercel flow
- Update FAQ to recommend Railway for most use cases

Railway executor features:
- Zero-dependency Node.js HTTP server
- Docker support via included Dockerfile
- Health check endpoint at /health
- Tool execution at /execute-tool
- API key authentication support
- Auto-restart on failure via railway.json
This commit is contained in:
Ajax Davis 2026-02-03 22:19:41 +10:00
parent 3f62228c56
commit ffc6ddcdbb
9 changed files with 1251 additions and 19 deletions

View file

@ -119,7 +119,54 @@ export default function ExecutorsDocsPage(): React.ReactElement {
needs:
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Railway Card */}
<Link
href="/docs/executors/railway"
className="group p-6 bg-surface border border-border rounded-lg hover:border-primary/50 transition-colors"
>
<div className="flex items-start gap-4">
<div className="w-12 h-12 bg-[#0B0D0E] rounded-lg flex items-center justify-center">
<svg
className="w-7 h-7"
viewBox="0 0 24 24"
fill="none"
role="img"
aria-labelledby="railway-logo-title"
>
<title id="railway-logo-title">Railway logo</title>
<path
d="M.113 12.611c-.139.312-.107.633.092.917.156.222.38.35.651.357h5.523a.28.28 0 00.216-.1c.07-.078.135-.22.2-.426.224-.718.428-1.09.65-1.492l.003-.005c.247-.448.51-.924.817-1.762.26-.712.296-1.371.113-2.008-.178-.62-.627-1.213-1.406-1.823a.248.248 0 00-.23-.044c-.078.025-.14.08-.178.157l-5.46 4.67a1.017 1.017 0 00-.287.41z"
fill="#fff"
/>
</svg>
</div>
<div className="flex-1">
<h3 className="font-semibold text-foreground group-hover:text-primary transition-colors">
Railway
</h3>
<p className="text-sm text-foreground-secondary mt-1">
Always-on with auto-scaling and a $5/month free tier. One-click deploy.
</p>
<div className="flex flex-wrap gap-2 mt-3">
<span className="px-2 py-0.5 text-xs bg-success/10 text-success rounded">
Official
</span>
<span className="px-2 py-0.5 text-xs bg-surface-secondary rounded text-foreground-tertiary">
Free tier
</span>
<span className="px-2 py-0.5 text-xs bg-surface-secondary rounded text-foreground-tertiary">
Auto-scaling
</span>
</div>
</div>
<Icon
icon="chevronRight"
className="w-5 h-5 text-foreground-tertiary group-hover:text-primary transition-colors"
/>
</div>
</Link>
{/* Unsandbox Card */}
<Link
href="/docs/executors/unsandbox"
@ -138,9 +185,6 @@ export default function ExecutorsDocsPage(): React.ReactElement {
command.
</p>
<div className="flex flex-wrap gap-2 mt-3">
<span className="px-2 py-0.5 text-xs bg-success/10 text-success rounded">
Recommended
</span>
<span className="px-2 py-0.5 text-xs bg-surface-secondary rounded text-foreground-tertiary">
No cold starts
</span>
@ -167,8 +211,10 @@ export default function ExecutorsDocsPage(): React.ReactElement {
className="w-6 h-6 text-white"
viewBox="0 0 76 65"
fill="currentColor"
aria-label="Vercel logo"
role="img"
aria-labelledby="vercel-logo-title"
>
<title id="vercel-logo-title">Vercel logo</title>
<path d="M37.5274 0L75.0548 65H0L37.5274 0Z" />
</svg>
</div>
@ -177,15 +223,14 @@ export default function ExecutorsDocsPage(): React.ReactElement {
Vercel
</h3>
<p className="text-sm text-foreground-secondary mt-1">
Serverless execution with VM-level isolation using Vercel Sandbox. One-click
deploy.
Serverless execution with VM-level isolation using Vercel Sandbox.
</p>
<div className="flex flex-wrap gap-2 mt-3">
<span className="px-2 py-0.5 text-xs bg-surface-secondary rounded text-foreground-tertiary">
One-click deploy
</span>
<span className="px-2 py-0.5 text-xs bg-surface-secondary rounded text-foreground-tertiary">
Free tier available
Pay-per-use
</span>
</div>
</div>
@ -211,6 +256,7 @@ export default function ExecutorsDocsPage(): React.ReactElement {
<thead>
<tr className="border-b border-border">
<th className="text-left py-3 pr-4 font-medium text-foreground">Feature</th>
<th className="text-left py-3 px-4 font-medium text-foreground">Railway</th>
<th className="text-left py-3 px-4 font-medium text-foreground">Unsandbox</th>
<th className="text-left py-3 pl-4 font-medium text-foreground">Vercel</th>
</tr>
@ -218,12 +264,14 @@ export default function ExecutorsDocsPage(): React.ReactElement {
<tbody className="text-foreground-secondary">
<tr className="border-b border-border/50">
<td className="py-3 pr-4">Deploy method</td>
<td className="py-3 px-4">One-click / CLI</td>
<td className="py-3 px-4">CLI command</td>
<td className="py-3 pl-4">One-click button</td>
</tr>
<tr className="border-b border-border/50">
<td className="py-3 pr-4">Isolation</td>
<td className="py-3 px-4">Container-level</td>
<td className="py-3 px-4">Container-level</td>
<td className="py-3 pl-4">VM-level (Sandbox)</td>
</tr>
<tr className="border-b border-border/50">
@ -231,6 +279,9 @@ export default function ExecutorsDocsPage(): React.ReactElement {
<td className="py-3 px-4">
<span className="text-success">None (always-on)</span>
</td>
<td className="py-3 px-4">
<span className="text-success">None (always-on)</span>
</td>
<td className="py-3 pl-4">Yes (serverless)</td>
</tr>
<tr className="border-b border-border/50">
@ -238,22 +289,44 @@ export default function ExecutorsDocsPage(): React.ReactElement {
<td className="py-3 px-4">
<span className="text-success">Unlimited</span>
</td>
<td className="py-3 px-4">
<span className="text-success">Unlimited</span>
</td>
<td className="py-3 pl-4">45min (Hobby) / 5hr (Pro)</td>
</tr>
<tr className="border-b border-border/50">
<td className="py-3 pr-4">Free tier</td>
<td className="py-3 px-4">
<span className="text-success">$5/month credit</span>
</td>
<td className="py-3 px-4">None</td>
<td className="py-3 pl-4">Limited</td>
</tr>
<tr className="border-b border-border/50">
<td className="py-3 pr-4">Pricing</td>
<td className="py-3 px-4">Per usage</td>
<td className="py-3 px-4">Per uptime</td>
<td className="py-3 pl-4">Per compute time</td>
</tr>
<tr className="border-b border-border/50">
<td className="py-3 pr-4">Auto-scaling</td>
<td className="py-3 px-4">
<span className="text-success">Yes</span>
</td>
<td className="py-3 px-4">Manual</td>
<td className="py-3 pl-4">Yes</td>
</tr>
<tr className="border-b border-border/50">
<td className="py-3 pr-4">Custom domains</td>
<td className="py-3 px-4">Yes</td>
<td className="py-3 px-4">Yes</td>
<td className="py-3 pl-4">Yes</td>
</tr>
<tr>
<td className="py-3 pr-4">Freeze/unfreeze</td>
<td className="py-3 px-4">Yes (save costs)</td>
<td className="py-3 pl-4">N/A (serverless)</td>
<td className="py-3 pr-4">Docker support</td>
<td className="py-3 px-4">Yes</td>
<td className="py-3 px-4">Yes</td>
<td className="py-3 pl-4">No</td>
</tr>
</tbody>
</table>
@ -387,18 +460,18 @@ export default function ExecutorsDocsPage(): React.ReactElement {
Which platform should I choose?
</h3>
<p className="text-foreground-secondary text-sm">
<strong>Unsandbox</strong> is recommended for most use cases. It has no cold
starts, unlimited runtime, and simple CLI deployment. Use <strong>Vercel</strong>{' '}
if you&apos;re already on Vercel or prefer one-click deployment and pay-per-use
pricing.
<strong>Railway</strong> is our official recommendation. It offers one-click
deployment, no cold starts, auto-scaling, and a generous $5/month free tier. Use{' '}
<strong>Unsandbox</strong> if you prefer CLI deployment, or{' '}
<strong>Vercel</strong> if you&apos;re already on Vercel and prefer pay-per-use
serverless pricing.
</p>
</div>
<div>
<h3 className="font-medium text-foreground mb-2">Can I use other platforms?</h3>
<p className="text-foreground-secondary text-sm">
Yes! Any platform that runs Node.js and exposes HTTP endpoints works. AWS Lambda,
Google Cloud Run, Railway, Render, Fly.iojust implement the API specification
above.
Google Cloud Run, Render, Fly.iojust implement the API specification above.
</p>
</div>
<div>

View file

@ -0,0 +1,442 @@
import { Button } from '@tpmjs/ui/Button/Button';
import { CodeBlock } from '@tpmjs/ui/CodeBlock/CodeBlock';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import type { Metadata } from 'next';
import Link from 'next/link';
import { AppFooter } from '~/components/AppFooter';
import { AppHeader } from '~/components/AppHeader';
export const metadata: Metadata = {
title: 'Deploy to Railway - Custom Executors - TPMJS',
description:
'Deploy a TPMJS executor to Railway with one click. Always-on, auto-scaling, with a generous free tier.',
};
const healthCheck = `curl https://your-executor.up.railway.app/health`;
const healthResponse = `{
"status": "ok",
"version": "1.0.0",
"info": {
"runtime": "railway",
"timestamp": "2024-01-01T00:00:00.000Z",
"region": "us-west1"
}
}`;
const executeExample = `curl -X POST https://your-executor.up.railway.app/execute-tool \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer your-api-key" \\
-d '{
"packageName": "@tpmjs/hello",
"name": "helloWorldTool",
"version": "latest",
"params": { "includeTimestamp": true }
}'`;
const cliDeploy = `# Clone the template
git clone https://github.com/tpmjs/tpmjs.git
cd tpmjs/templates/railway-executor
# Install Railway CLI
npm install -g @railway/cli
# Login to Railway
railway login
# Create a new project and deploy
railway init
railway up`;
const envVars = `# Set environment variables via CLI
railway variables set EXECUTOR_API_KEY=your-secure-key
railway variables set OPENAI_API_KEY=sk-xxx
railway variables set DATABASE_URL=postgres://...`;
const localDev = `# Clone the repository
git clone https://github.com/tpmjs/tpmjs.git
cd tpmjs/templates/railway-executor
# Run locally
PORT=3000 node index.js
# Test health endpoint
curl http://localhost:3000/health`;
const dockerDeploy = `# Build the image
docker build -t tpmjs-executor .
# Run locally
docker run -p 3000:3000 -e EXECUTOR_API_KEY=your-key tpmjs-executor`;
export default function RailwayExecutorPage(): React.ReactElement {
return (
<div className="min-h-screen bg-background flex flex-col">
<AppHeader />
<main className="flex-1">
<div className="max-w-4xl mx-auto px-4 py-12">
{/* Breadcrumb */}
<nav className="flex items-center gap-2 text-sm text-foreground-secondary mb-8">
<Link href="/docs/executors" className="hover:text-foreground transition-colors">
Executors
</Link>
<Icon icon="chevronRight" className="w-4 h-4" />
<span className="text-foreground">Railway</span>
</nav>
{/* Header */}
<div className="mb-12">
<div className="flex items-center gap-4 mb-4">
<div className="w-12 h-12 bg-[#0B0D0E] rounded-lg flex items-center justify-center">
<svg
className="w-7 h-7"
viewBox="0 0 24 24"
fill="none"
role="img"
aria-labelledby="railway-header-logo"
>
<title id="railway-header-logo">Railway logo</title>
<path
d="M.113 12.611c-.139.312-.107.633.092.917.156.222.38.35.651.357h5.523a.28.28 0 00.216-.1c.07-.078.135-.22.2-.426.224-.718.428-1.09.65-1.492l.003-.005c.247-.448.51-.924.817-1.762.26-.712.296-1.371.113-2.008-.178-.62-.627-1.213-1.406-1.823a.248.248 0 00-.23-.044c-.078.025-.14.08-.178.157l-5.46 4.67a1.017 1.017 0 00-.287.41z"
fill="#fff"
/>
<path
d="M8.904 6.16a.258.258 0 00-.22.108c-.06.08-.08.178-.053.27.147.517.147 1.023 0 1.56-.114.416-.304.81-.49 1.194l-.04.083c-.214.443-.43.889-.595 1.42-.204.66-.21 1.336-.017 2.015.287 1.013.986 1.826 1.93 2.252l.014.006c.15.07.252.116.341.196.09.08.17.195.28.368l.023.035c.217.34.517.81.827 1.142a.244.244 0 00.204.091h1.694a.252.252 0 00.217-.12.234.234 0 00.006-.243 7.639 7.639 0 00-.66-1.016c-.253-.34-.523-.665-.76-.948a6.876 6.876 0 01-.507-.66c-.15-.228-.288-.53-.296-.857-.016-.679.413-1.39.85-1.994.168-.232.35-.484.52-.767.306-.51.383-1.054.227-1.62a2.568 2.568 0 00-.943-1.317 6.487 6.487 0 00-.98-.636 2.27 2.27 0 01-.516-.346.248.248 0 00-.214-.073l-.842.057z"
fill="#fff"
/>
<path
d="M20.756 6.225h-6.243a.255.255 0 00-.23.146.243.243 0 00.035.266c.32.362.618.742.892 1.137.317.46.597.946.833 1.45a.243.243 0 00.218.142h5.808c.278-.005.507-.133.667-.36.203-.287.234-.62.091-.94l-.765-1.563a.264.264 0 00-.24-.152l-1.066-.126z"
fill="#fff"
/>
<path
d="M15.82 11.152a.247.247 0 00-.229.158c-.243.648-.36.972-.618 1.46-.257.49-.527.84-1.058 1.468a.244.244 0 00.006.325c.235.264.496.505.78.722.425.327.883.608 1.365.837a.237.237 0 00.108.026h4.16a.255.255 0 00.229-.146.243.243 0 00-.035-.267c-.444-.502-.8-.98-1.172-1.632l-.006-.01c-.367-.642-.57-1.092-.83-1.872a.243.243 0 00-.232-.169l-2.468-.9z"
fill="#fff"
/>
<path
d="M13.873 16.68a.25.25 0 00-.2.074 4.588 4.588 0 01-.637.52c-.456.314-.797.472-1.236.647a.24.24 0 00-.147.137.249.249 0 00.004.2l.463.95c.098.2.285.32.5.317h6.357c.273-.013.497-.144.653-.374.198-.29.223-.622.074-.935l-.553-1.131a.257.257 0 00-.236-.152l-5.042-.253z"
fill="#fff"
/>
</svg>
</div>
<div>
<h1 className="text-3xl font-bold text-foreground">Deploy to Railway</h1>
<p className="text-foreground-secondary">
Always-on execution with auto-scaling and a free tier
</p>
</div>
</div>
</div>
{/* Why Railway */}
<section className="mb-12">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="p-4 bg-surface border border-border rounded-lg">
<div className="text-2xl mb-2">$5</div>
<div className="text-sm text-foreground-secondary">Free monthly credit</div>
</div>
<div className="p-4 bg-surface border border-border rounded-lg">
<div className="text-2xl mb-2">0ms</div>
<div className="text-sm text-foreground-secondary">No cold starts</div>
</div>
<div className="p-4 bg-surface border border-border rounded-lg">
<div className="text-2xl mb-2">Auto</div>
<div className="text-sm text-foreground-secondary">Scaling built-in</div>
</div>
</div>
</section>
{/* One-Click Deploy */}
<section className="mb-12">
<h2 className="text-2xl font-semibold text-foreground mb-4">One-Click Deploy</h2>
<p className="text-foreground-secondary mb-6">
Deploy the TPMJS executor to Railway with a single click:
</p>
<a
href="https://railway.app/template/tpmjs-executor?referralCode=tpmjs"
target="_blank"
rel="noopener noreferrer"
>
<Button size="lg">
<svg
className="w-4 h-4 mr-2"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path d="M.113 12.611c-.139.312-.107.633.092.917.156.222.38.35.651.357h5.523a.28.28 0 00.216-.1c.07-.078.135-.22.2-.426.224-.718.428-1.09.65-1.492l.003-.005c.247-.448.51-.924.817-1.762.26-.712.296-1.371.113-2.008-.178-.62-.627-1.213-1.406-1.823a.248.248 0 00-.23-.044c-.078.025-.14.08-.178.157l-5.46 4.67a1.017 1.017 0 00-.287.41z" />
</svg>
Deploy on Railway
</Button>
</a>
<p className="text-sm text-foreground-tertiary mt-4">
After deployment, your executor will be available at{' '}
<code className="px-1.5 py-0.5 bg-surface rounded">
https://your-project.up.railway.app
</code>
</p>
</section>
{/* CLI Deploy */}
<section className="mb-12">
<h2 className="text-2xl font-semibold text-foreground mb-4">Deploy via CLI</h2>
<p className="text-foreground-secondary mb-4">
Prefer the command line? Deploy with the Railway CLI:
</p>
<CodeBlock language="bash" code={cliDeploy} />
</section>
{/* Test Your Deployment */}
<section className="mb-12">
<h2 className="text-2xl font-semibold text-foreground mb-4">Test Your Deployment</h2>
<p className="text-foreground-secondary mb-4">Verify your executor is running:</p>
<CodeBlock language="bash" code={healthCheck} />
<p className="text-sm text-foreground-secondary mt-4 mb-2">Expected response:</p>
<CodeBlock language="json" code={healthResponse} />
</section>
{/* Authentication */}
<section className="mb-12">
<h2 className="text-2xl font-semibold text-foreground mb-4">Add Authentication</h2>
<div className="p-4 bg-warning/10 border border-warning/30 rounded-lg mb-4">
<p className="text-sm text-warning">
<strong>Important:</strong> Without an API key, anyone can execute tools on your
executor. Always set{' '}
<code className="px-1 bg-warning/20 rounded">EXECUTOR_API_KEY</code> in production.
</p>
</div>
<ol className="text-foreground-secondary space-y-3">
<li className="flex gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-surface-secondary text-foreground-secondary text-sm flex items-center justify-center">
1
</span>
<span>Go to your Railway project dashboard</span>
</li>
<li className="flex gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-surface-secondary text-foreground-secondary text-sm flex items-center justify-center">
2
</span>
<span>Click on your service, then go to &quot;Variables&quot;</span>
</li>
<li className="flex gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-surface-secondary text-foreground-secondary text-sm flex items-center justify-center">
3
</span>
<span>
Add <code className="px-1 bg-surface rounded">EXECUTOR_API_KEY</code> with a
secure random value
</span>
</li>
<li className="flex gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-surface-secondary text-foreground-secondary text-sm flex items-center justify-center">
4
</span>
<span>Railway will automatically redeploy with the new variable</span>
</li>
</ol>
</section>
{/* Environment Variables */}
<section className="mb-12">
<h2 className="text-2xl font-semibold text-foreground mb-4">Environment Variables</h2>
<p className="text-foreground-secondary mb-4">
Add environment variables via the Railway dashboard or CLI:
</p>
<CodeBlock language="bash" code={envVars} />
<p className="text-sm text-foreground-tertiary mt-4">
These variables will be available during tool execution.
</p>
</section>
{/* Execute a Tool */}
<section className="mb-12">
<h2 className="text-2xl font-semibold text-foreground mb-4">Execute a Tool</h2>
<p className="text-foreground-secondary mb-4">
Test tool execution with a curl request:
</p>
<CodeBlock language="bash" code={executeExample} />
</section>
{/* Local Development */}
<section className="mb-12">
<h2 className="text-2xl font-semibold text-foreground mb-4">Local Development</h2>
<p className="text-foreground-secondary mb-4">Run the executor locally for testing:</p>
<CodeBlock language="bash" code={localDev} />
</section>
{/* Docker */}
<section className="mb-12">
<h2 className="text-2xl font-semibold text-foreground mb-4">Docker Deployment</h2>
<p className="text-foreground-secondary mb-4">
The template includes a Dockerfile for container deployments:
</p>
<CodeBlock language="bash" code={dockerDeploy} />
<p className="text-sm text-foreground-tertiary mt-4">
Railway will automatically detect and use the Dockerfile if present.
</p>
</section>
{/* How It Works */}
<section className="mb-12">
<h2 className="text-2xl font-semibold text-foreground mb-4">How It Works</h2>
<p className="text-foreground-secondary mb-4">
The Railway executor runs as an always-on Node.js service:
</p>
<ol className="text-foreground-secondary space-y-3">
<li className="flex gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-primary/10 text-primary text-sm flex items-center justify-center">
1
</span>
<span>
Receives tool execution request via HTTP POST to{' '}
<code className="px-1 bg-surface rounded">/execute-tool</code>
</span>
</li>
<li className="flex gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-primary/10 text-primary text-sm flex items-center justify-center">
2
</span>
<span>Creates an isolated temporary directory for the execution</span>
</li>
<li className="flex gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-primary/10 text-primary text-sm flex items-center justify-center">
3
</span>
<span>
Installs the npm package using{' '}
<code className="px-1 bg-surface rounded">npm install</code>
</span>
</li>
<li className="flex gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-primary/10 text-primary text-sm flex items-center justify-center">
4
</span>
<span>
Loads the tool and calls its{' '}
<code className="px-1 bg-surface rounded">execute()</code> function
</span>
</li>
<li className="flex gap-3">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-primary/10 text-primary text-sm flex items-center justify-center">
5
</span>
<span>Returns the result and cleans up the temporary directory</span>
</li>
</ol>
</section>
{/* Security */}
<section className="mb-12">
<h2 className="text-2xl font-semibold text-foreground mb-4">Security</h2>
<ul className="text-foreground-secondary space-y-2">
<li className="flex items-start gap-2">
<Icon icon="check" className="w-5 h-5 text-success flex-shrink-0 mt-0.5" />
<span>
Set <code className="px-1 bg-surface rounded">EXECUTOR_API_KEY</code> to require
authentication
</span>
</li>
<li className="flex items-start gap-2">
<Icon icon="check" className="w-5 h-5 text-success flex-shrink-0 mt-0.5" />
<span>Each tool execution uses an isolated temporary directory</span>
</li>
<li className="flex items-start gap-2">
<Icon icon="check" className="w-5 h-5 text-success flex-shrink-0 mt-0.5" />
<span>Environment variables stored encrypted by Railway</span>
</li>
<li className="flex items-start gap-2">
<Icon icon="check" className="w-5 h-5 text-success flex-shrink-0 mt-0.5" />
<span>All traffic encrypted via HTTPS</span>
</li>
<li className="flex items-start gap-2">
<Icon icon="check" className="w-5 h-5 text-success flex-shrink-0 mt-0.5" />
<span>Auto-restart on failure for high availability</span>
</li>
</ul>
</section>
{/* Pricing */}
<section className="mb-12">
<h2 className="text-2xl font-semibold text-foreground mb-4">Pricing</h2>
<p className="text-foreground-secondary mb-4">
Railway offers usage-based pricing with a generous free tier:
</p>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-left py-3 pr-4 font-medium text-foreground">Tier</th>
<th className="text-left py-3 px-4 font-medium text-foreground">Price</th>
<th className="text-left py-3 pl-4 font-medium text-foreground">Includes</th>
</tr>
</thead>
<tbody className="text-foreground-secondary">
<tr className="border-b border-border/50">
<td className="py-3 pr-4">Free Tier</td>
<td className="py-3 px-4">$0/month</td>
<td className="py-3 pl-4">$5 credit, enough for light usage</td>
</tr>
<tr>
<td className="py-3 pr-4">Usage-based</td>
<td className="py-3 px-4">~$0.000463/min</td>
<td className="py-3 pl-4">0.5 vCPU, 512MB RAM</td>
</tr>
</tbody>
</table>
</div>
<p className="text-sm text-foreground-tertiary mt-4">
See{' '}
<a
href="https://railway.app/pricing"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Railway Pricing
</a>{' '}
for current rates.
</p>
</section>
{/* Connect to TPMJS */}
<section className="mb-12 p-6 bg-primary/5 border border-primary/20 rounded-lg">
<h2 className="text-lg font-semibold text-foreground mb-4">Connect to TPMJS</h2>
<ol className="text-foreground-secondary space-y-2">
<li>1. Go to your collection or agent settings on TPMJS</li>
<li>2. Select &quot;Custom Executor&quot; in Executor Configuration</li>
<li>
3. Enter URL:{' '}
<code className="px-1.5 py-0.5 bg-surface rounded">
https://your-project.up.railway.app
</code>
</li>
<li>4. Enter your API key (if configured)</li>
<li>5. Click &quot;Verify Connection&quot;</li>
</ol>
</section>
{/* Navigation */}
<div className="flex items-center justify-between pt-8 border-t border-border">
<Link
href="/docs/executors"
className="flex items-center gap-2 text-foreground-secondary hover:text-foreground transition-colors"
>
<Icon icon="chevronLeft" className="w-4 h-4" />
<span>Back to Executors</span>
</Link>
<Link
href="/docs/executors/unsandbox"
className="flex items-center gap-2 text-foreground-secondary hover:text-foreground transition-colors"
>
<span>Unsandbox Guide</span>
<Icon icon="chevronRight" className="w-4 h-4" />
</Link>
</div>
</div>
</main>
<AppFooter />
</div>
);
}

View file

@ -397,11 +397,11 @@ export default function UnsandboxExecutorPage(): React.ReactElement {
{/* Navigation */}
<div className="flex items-center justify-between pt-8 border-t border-border">
<Link
href="/docs/executors"
href="/docs/executors/railway"
className="flex items-center gap-2 text-foreground-secondary hover:text-foreground transition-colors"
>
<Icon icon="chevronLeft" className="w-4 h-4" />
<span>Back to Executors</span>
<span>Railway Guide</span>
</Link>
<Link
href="/docs/executors/vercel"

4
templates/railway-executor/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
node_modules/
.env
.env.local
*.log

View file

@ -0,0 +1,22 @@
FROM node:20-slim
# Install npm for tool installation during execution
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy executor files
COPY package.json index.js ./
# Create temp directory for tool executions
RUN mkdir -p /tmp
# Set environment
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
CMD ["node", "index.js"]

View file

@ -0,0 +1,260 @@
# TPMJS Executor for Railway
Deploy your own TPMJS tool executor on **Railway** for reliable, always-on execution.
## Features
- **One-Click Deploy**: Deploy to Railway in seconds
- **Always-On**: No cold starts, instant tool execution
- **Full Control**: Your infrastructure, your environment variables
- **Privacy**: No data passes through TPMJS servers
- **Auto-Scaling**: Railway handles scaling automatically
- **Free Tier**: $5/month free credit included
## One-Click Deploy
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/tpmjs-executor?referralCode=tpmjs)
Or deploy manually:
```bash
# Clone this template
git clone https://github.com/tpmjs/tpmjs.git
cd tpmjs/templates/railway-executor
# Create a new Railway project
railway init
# Deploy
railway up
```
Your executor will be available at: `https://your-project.up.railway.app`
## How It Works
This executor runs as an always-on Node.js service on Railway:
1. Receives tool execution requests via HTTP
2. Installs the npm package in an isolated directory
3. Executes the tool with your parameters
4. Returns the result and cleans up
## API Endpoints
### GET /health
Check executor health status.
```bash
curl https://your-executor.up.railway.app/health
```
**Response:**
```json
{
"status": "ok",
"version": "1.0.0",
"info": {
"runtime": "railway",
"timestamp": "2024-01-01T00:00:00.000Z",
"region": "us-west1"
}
}
```
### POST /execute-tool
Execute a TPMJS tool.
```bash
curl -X POST https://your-executor.up.railway.app/execute-tool \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-api-key" \
-d '{
"packageName": "@tpmjs/hello",
"name": "helloWorldTool",
"version": "latest",
"params": { "includeTimestamp": true }
}'
```
**Response:**
```json
{
"success": true,
"output": {
"message": "Hello, World!",
"timestamp": "2024-01-01T00:00:00.000Z"
},
"executionTimeMs": 2345
}
```
## Configuration
### Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `EXECUTOR_API_KEY` | No | API key for authentication. If set, requests must include `Authorization: Bearer <key>` header. |
### Setting Up API Key Authentication
1. Go to your Railway project dashboard
2. Click on your service
3. Go to "Variables" tab
4. Add `EXECUTOR_API_KEY` with a secure random value
5. The service will automatically redeploy
### Adding Tool Environment Variables
Pass environment variables that your tools need:
1. In Railway dashboard, go to "Variables"
2. Add your variables (e.g., `OPENAI_API_KEY`, `DATABASE_URL`)
3. These will be available during tool execution
Or use the Railway CLI:
```bash
railway variables set EXECUTOR_API_KEY=your-key
railway variables set OPENAI_API_KEY=sk-xxx
railway variables set DATABASE_URL=postgres://...
```
## Connecting to TPMJS
1. Go to your TPMJS collection or agent settings
2. In "Executor Configuration", select "Custom Executor"
3. Enter your executor URL: `https://your-project.up.railway.app`
4. Enter your API key (if configured)
5. Click "Verify Connection" to test
## Local Development
```bash
# Clone the repository
git clone https://github.com/tpmjs/tpmjs.git
cd tpmjs/templates/railway-executor
# Run locally
PORT=3000 node index.js
# Or with an API key
EXECUTOR_API_KEY=test-key PORT=3000 node index.js
# Test health endpoint
curl http://localhost:3000/health
# Test tool execution
curl -X POST http://localhost:3000/execute-tool \
-H "Content-Type: application/json" \
-d '{
"packageName": "@tpmjs/hello",
"name": "helloWorldTool",
"params": {}
}'
```
## Managing Your Service
### View Logs
```bash
railway logs
```
Or view in the Railway dashboard under "Deployments" → select deployment → "Logs"
### Redeploy
```bash
railway up
```
Or push to your connected GitHub repository for automatic deployments.
### Scale Resources
1. Go to Railway dashboard
2. Click on your service
3. Go to "Settings" tab
4. Adjust CPU and memory limits
### Custom Domains
1. Go to Railway dashboard
2. Click on your service
3. Go to "Settings" tab
4. Under "Domains", click "Generate Domain" or add a custom domain
## Docker Deployment
If you prefer Docker:
```bash
# Build the image
docker build -t tpmjs-executor .
# Run locally
docker run -p 3000:3000 -e EXECUTOR_API_KEY=your-key tpmjs-executor
```
Railway will automatically detect and use the Dockerfile if present.
## Security
- Set `EXECUTOR_API_KEY` to require authentication for all requests
- Tools run in isolated temporary directories
- Each execution uses a fresh npm install
- Environment variables are stored encrypted by Railway
- Network traffic is encrypted via HTTPS
## Pricing
Railway pricing is usage-based with a generous free tier:
- **Free Tier**: $5/month credit (enough for light usage)
- **Pay-as-you-go**: ~$0.000463/min for 0.5 vCPU, 512MB RAM
See [Railway Pricing](https://railway.app/pricing) for current rates.
**Cost Optimization Tips:**
- Use the "Sleep" feature for dev environments
- Set memory limits appropriate for your tools
- Monitor usage in Railway dashboard
## Comparison: Railway vs Other Platforms
| Feature | Railway | Vercel | Unsandbox |
|---------|---------|--------|-----------|
| Deploy method | One-click / CLI | One-click | CLI |
| Cold starts | None (always-on) | Yes (serverless) | None |
| Max runtime | Unlimited | 45min / 5hr | Unlimited |
| Free tier | $5/month credit | Limited | None |
| Pricing | Per usage | Per compute | Per uptime |
| Docker support | Yes | No | Yes |
| Auto-scaling | Yes | Yes | Manual |
## Troubleshooting
### "Connection refused" errors
- Check that your service is running in Railway dashboard
- Verify the URL is correct (check "Domains" in settings)
- Ensure `EXECUTOR_API_KEY` matches if authentication is enabled
### Tool installation failures
- Check Railway logs for npm errors
- Verify the package name and version are correct
- Some packages may need additional system dependencies
### Timeout errors
- Railway has no timeout limit, but individual tool executions timeout at 2 minutes
- For longer-running tools, consider increasing the timeout in the executor code
## Support
- [TPMJS Custom Executors Documentation](https://tpmjs.com/docs/executors)
- [Railway Documentation](https://docs.railway.app)
- [GitHub Issues](https://github.com/tpmjs/tpmjs/issues)

View file

@ -0,0 +1,398 @@
#!/usr/bin/env node
/**
* TPMJS Executor for Railway
*
* A lightweight HTTP server that executes TPMJS tools.
* Designed for deployment on Railway with zero dependencies.
*
* API-compatible with the Vercel and Unsandbox executors.
*/
const http = require('node:http');
const { execSync, spawn } = require('node:child_process');
const fs = require('node:fs');
const path = require('node:path');
const PORT = process.env.PORT || 3000;
const API_KEY = process.env.EXECUTOR_API_KEY || null;
// CORS headers for cross-origin requests
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
};
/**
* Send JSON response with proper headers
*/
function jsonResponse(res, statusCode, data) {
res.writeHead(statusCode, {
'Content-Type': 'application/json',
...corsHeaders,
});
res.end(JSON.stringify(data));
}
/**
* Validate API key if configured
*/
function checkAuth(req) {
if (!API_KEY) return true;
const authHeader = req.headers.authorization;
return authHeader === `Bearer ${API_KEY}`;
}
/**
* Parse JSON request body
*/
function parseBody(req) {
return new Promise((resolve, reject) => {
let body = '';
req.on('data', (chunk) => {
body += chunk;
});
req.on('end', () => {
try {
resolve(body ? JSON.parse(body) : {});
} catch (_e) {
reject(new Error('Invalid JSON'));
}
});
req.on('error', reject);
});
}
/**
* GET /health - Health check endpoint
*/
function handleHealth(_req, res) {
jsonResponse(res, 200, {
status: 'ok',
version: '1.0.0',
info: {
runtime: 'railway',
timestamp: new Date().toISOString(),
region: process.env.RAILWAY_REGION || 'unknown',
},
});
}
/**
* Create isolated work directory and package.json
*/
function createWorkDir() {
const workDir = `/tmp/tpmjs-exec-${Date.now()}-${Math.random().toString(36).slice(2)}`;
fs.mkdirSync(workDir, { recursive: true });
fs.writeFileSync(
path.join(workDir, 'package.json'),
JSON.stringify({
name: 'tpmjs-execution',
private: true,
type: 'commonjs',
})
);
return workDir;
}
/**
* Install npm package in work directory
*/
function installPackage(workDir, packageSpec) {
execSync(`npm install --no-save --omit=dev --no-audit --no-fund ${packageSpec}`, {
cwd: workDir,
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 60000, // 60s timeout for install
});
}
/**
* Generate the tool execution script
*/
function generateExecutionScript(packageName, name, params, env) {
const envSetup = env
? Object.entries(env)
.map(([key, value]) => `process.env[${JSON.stringify(key)}] = ${JSON.stringify(value)};`)
.join('\n')
: '';
return `
${envSetup}
(async () => {
try {
const pkg = require(${JSON.stringify(packageName)});
// Find the tool export - check named export, default.name, or default
let tool = pkg[${JSON.stringify(name)}] || pkg.default?.[${JSON.stringify(name)}] || pkg.default;
if (!tool) {
throw new Error(\`Tool "${name}" not found in package "${packageName}"\`);
}
// Handle factory functions (tools that need to be instantiated)
if (typeof tool === 'function' && !tool.execute) {
const envVars = ${env ? JSON.stringify(env) : 'null'};
// Try no-arg call first
try {
const result = tool();
if (result && typeof result.execute === 'function') {
tool = result;
}
} catch {}
// Try with env config if still a function
if (typeof tool === 'function' && envVars) {
try {
const result = tool(envVars);
if (result && typeof result.execute === 'function') {
tool = result;
}
} catch {}
}
}
if (!tool || typeof tool.execute !== 'function') {
throw new Error(\`Tool "${name}" does not have an execute() function\`);
}
// Execute the tool
const result = await tool.execute(${JSON.stringify(params)});
process.stdout.write(JSON.stringify({ __tpmjs_result__: result }));
} catch (err) {
process.stderr.write(JSON.stringify({ __tpmjs_error__: err.message || String(err) }));
process.exitCode = 1;
}
})();
`.trim();
}
/**
* Run execution script and return results
*/
function runScript(workDir, env) {
return new Promise((resolve) => {
const child = spawn('node', ['execute.cjs'], {
cwd: workDir,
env: { ...process.env, ...env },
timeout: 120000, // 2 minute timeout
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
stdout += data;
});
child.stderr.on('data', (data) => {
stderr += data;
});
child.on('close', (code) => {
resolve({ exitCode: code, stdout, stderr });
});
child.on('error', (err) => {
resolve({ exitCode: 1, stdout: '', stderr: err.message });
});
});
}
/**
* Clean up work directory
*/
function cleanup(workDir) {
try {
fs.rmSync(workDir, { recursive: true, force: true });
} catch (_e) {
// Ignore cleanup errors
}
}
/**
* Parse execution result and determine response
*/
function parseExecutionResult(result, startTime) {
// Handle execution failure
if (result.exitCode !== 0) {
// Try to parse structured error from stderr
try {
const errorObj = JSON.parse(result.stderr);
if (errorObj.__tpmjs_error__) {
return {
success: false,
error: errorObj.__tpmjs_error__,
executionTimeMs: Date.now() - startTime,
};
}
} catch (_e) {
// Not structured error
}
return {
success: false,
error: result.stderr || `Script exited with code ${result.exitCode}`,
executionTimeMs: Date.now() - startTime,
};
}
// Parse the result
try {
const parsed = JSON.parse(result.stdout);
if (parsed.__tpmjs_result__ !== undefined) {
return {
success: true,
output: parsed.__tpmjs_result__,
executionTimeMs: Date.now() - startTime,
};
}
} catch (_e) {
// Not structured result
}
// Return raw output
return {
success: true,
output: result.stdout || null,
stderr: result.stderr || undefined,
executionTimeMs: Date.now() - startTime,
};
}
/**
* POST /execute-tool - Execute a TPMJS tool
*/
async function handleExecuteTool(req, res) {
const startTime = Date.now();
// Check authorization
if (!checkAuth(req)) {
return jsonResponse(res, 401, {
success: false,
error: 'Unauthorized',
executionTimeMs: Date.now() - startTime,
});
}
// Parse request body
let body;
try {
body = await parseBody(req);
} catch (_e) {
return jsonResponse(res, 400, {
success: false,
error: 'Invalid JSON body',
executionTimeMs: Date.now() - startTime,
});
}
const { packageName, name, version = 'latest', params = {}, env } = body;
// Validate required fields
if (!packageName || !name) {
return jsonResponse(res, 400, {
success: false,
error: 'Missing required fields: packageName, name',
executionTimeMs: Date.now() - startTime,
});
}
const packageSpec = `${packageName}@${version}`;
const workDir = createWorkDir();
try {
// Install the npm package
console.log(`[executor] Installing ${packageSpec}...`);
const installStart = Date.now();
try {
installPackage(workDir, packageSpec);
} catch (installError) {
console.error(`[executor] npm install failed:`, installError.message);
cleanup(workDir);
return jsonResponse(res, 500, {
success: false,
error: `npm install failed: ${installError.message}`,
stderr: installError.stderr?.toString(),
executionTimeMs: Date.now() - startTime,
});
}
console.log(`[executor] npm install completed in ${Date.now() - installStart}ms`);
// Generate and write execution script
const script = generateExecutionScript(packageName, name, params, env);
fs.writeFileSync(path.join(workDir, 'execute.cjs'), script);
// Run the execution script
console.log(`[executor] Running tool ${packageName}/${name}...`);
const runStart = Date.now();
const result = await runScript(workDir, env);
console.log(
`[executor] Tool execution completed in ${Date.now() - runStart}ms (exit: ${result.exitCode})`
);
// Cleanup and return result
cleanup(workDir);
return jsonResponse(res, 200, parseExecutionResult(result, startTime));
} catch (error) {
cleanup(workDir);
return jsonResponse(res, 500, {
success: false,
error: error.message || String(error),
executionTimeMs: Date.now() - startTime,
});
}
}
/**
* Main HTTP server
*/
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://localhost:${PORT}`);
const pathname = url.pathname;
// Handle CORS preflight
if (req.method === 'OPTIONS') {
res.writeHead(200, corsHeaders);
return res.end();
}
// Route requests (support both /api/path and /path)
if ((pathname === '/api/health' || pathname === '/health') && req.method === 'GET') {
return handleHealth(req, res);
}
if ((pathname === '/api/execute-tool' || pathname === '/execute-tool') && req.method === 'POST') {
return handleExecuteTool(req, res);
}
// Root path - simple info
if (pathname === '/' && req.method === 'GET') {
return jsonResponse(res, 200, {
name: 'TPMJS Executor',
version: '1.0.0',
runtime: 'railway',
endpoints: {
health: 'GET /health',
execute: 'POST /execute-tool',
},
});
}
// 404 for unknown routes
jsonResponse(res, 404, { error: 'Not found' });
});
// Start server
server.listen(PORT, () => {
console.log(`TPMJS Executor running on port ${PORT}`);
console.log(`Health: http://localhost:${PORT}/health`);
console.log(`Execute: POST http://localhost:${PORT}/execute-tool`);
if (API_KEY) {
console.log(`Authentication: Required (EXECUTOR_API_KEY is set)`);
} else {
console.log(`Authentication: None (set EXECUTOR_API_KEY to enable)`);
}
});

View file

@ -0,0 +1,20 @@
{
"name": "tpmjs-executor",
"version": "1.0.0",
"private": true,
"description": "TPMJS Tool Executor for Railway - Deploy your own executor on Railway",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "node index.js"
},
"engines": {
"node": ">=18.0.0"
},
"dependencies": {},
"keywords": [
"tpmjs",
"executor",
"railway"
]
}

View file

@ -0,0 +1,13 @@
{
"$schema": "https://railway.app/railway.schema.json",
"build": {
"builder": "NIXPACKS"
},
"deploy": {
"startCommand": "node index.js",
"healthcheckPath": "/health",
"healthcheckTimeout": 30,
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 3
}
}