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

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
}
}