Skip to content

Commit 59280a6

Browse files
feat: Add Railway Redis and PostgreSQL integration
1 parent 1e1d160 commit 59280a6

File tree

2 files changed

+299
-0
lines changed

2 files changed

+299
-0
lines changed

src/servers/railway/config.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* Railway Server Configuration
3+
* Handles Railway-specific environment variables and service references
4+
*/
5+
6+
export interface RailwayConfig {
7+
// Server
8+
port: number;
9+
environment: string;
10+
11+
// Database
12+
databaseUrl: string;
13+
14+
// Redis
15+
redisUrl: string;
16+
redisHost: string;
17+
redisPort: number;
18+
redisUser?: string;
19+
redisPassword?: string;
20+
21+
// Auth
22+
authMode: string;
23+
apiKeySecret: string;
24+
jwtSecret: string;
25+
26+
// Features
27+
corsOrigins: string[];
28+
rateLimitEnabled: boolean;
29+
rateLimitFree: number;
30+
enableWebSocket: boolean;
31+
enableAnalytics: boolean;
32+
33+
// Storage
34+
storageMode: 'redis' | 'postgres' | 'hybrid';
35+
}
36+
37+
/**
38+
* Get Railway configuration with proper service references
39+
* Railway provides reference variables like ${{Postgres.DATABASE_URL}}
40+
*/
41+
export function getRailwayConfig(): RailwayConfig {
42+
// Parse CORS origins
43+
const corsOrigins = process.env.CORS_ORIGINS?.split(',') || [
44+
'https://claude.ai',
45+
'https://claude.anthropic.com',
46+
'http://localhost:3000'
47+
];
48+
49+
// Build Redis URL from Railway reference variables
50+
// Railway typically provides: REDIS_URL or individual components
51+
let redisUrl = process.env.REDIS_URL || '';
52+
53+
// If no REDIS_URL, try to build from components
54+
if (!redisUrl && process.env.REDISHOST) {
55+
const host = process.env.REDISHOST;
56+
const port = process.env.REDISPORT || '6379';
57+
const user = process.env.REDISUSER || 'default';
58+
const password = process.env.REDISPASSWORD || '';
59+
60+
if (password) {
61+
redisUrl = `redis://${user}:${password}@${host}:${port}`;
62+
} else {
63+
redisUrl = `redis://${host}:${port}`;
64+
}
65+
}
66+
67+
// Fallback to local Redis if not in production
68+
if (!redisUrl && process.env.NODE_ENV !== 'production') {
69+
redisUrl = 'redis://localhost:6379';
70+
}
71+
72+
return {
73+
// Server
74+
port: parseInt(process.env.PORT || '3000'),
75+
environment: process.env.NODE_ENV || 'development',
76+
77+
// Database - Railway provides this as DATABASE_URL
78+
databaseUrl: process.env.DATABASE_URL || 'postgresql://localhost:5432/stackmemory',
79+
80+
// Redis
81+
redisUrl,
82+
redisHost: process.env.REDISHOST || 'localhost',
83+
redisPort: parseInt(process.env.REDISPORT || '6379'),
84+
redisUser: process.env.REDISUSER,
85+
redisPassword: process.env.REDISPASSWORD,
86+
87+
// Auth
88+
authMode: process.env.AUTH_MODE || 'api_key',
89+
apiKeySecret: process.env.API_KEY_SECRET || 'development-secret',
90+
jwtSecret: process.env.JWT_SECRET || 'development-jwt-secret',
91+
92+
// Features
93+
corsOrigins,
94+
rateLimitEnabled: process.env.RATE_LIMIT_ENABLED === 'true',
95+
rateLimitFree: parseInt(process.env.RATE_LIMIT_FREE || '100'),
96+
enableWebSocket: process.env.ENABLE_WEBSOCKET !== 'false',
97+
enableAnalytics: process.env.ENABLE_ANALYTICS === 'true',
98+
99+
// Storage mode
100+
storageMode: (process.env.STORAGE_MODE as any) || 'hybrid'
101+
};
102+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Enhanced Railway Server with Redis and PostgreSQL
4+
*/
5+
6+
import express from 'express';
7+
import cors from 'cors';
8+
import { createClient } from 'redis';
9+
import pg from 'pg';
10+
import { getRailwayConfig } from './config.js';
11+
12+
const { Client } = pg;
13+
14+
async function startServer() {
15+
const config = getRailwayConfig();
16+
17+
console.log('🚀 Starting StackMemory Railway Server (Enhanced)');
18+
console.log(`📍 Environment: ${config.environment}`);
19+
console.log(`🔌 Port: ${config.port}`);
20+
21+
const app = express();
22+
23+
// Middleware
24+
app.use(cors({
25+
origin: config.corsOrigins,
26+
credentials: true
27+
}));
28+
app.use(express.json());
29+
30+
// Health check endpoint - Railway uses this
31+
app.get('/health', (req, res) => {
32+
res.json({
33+
status: 'healthy',
34+
service: 'stackmemory',
35+
timestamp: new Date().toISOString()
36+
});
37+
});
38+
39+
// Root endpoint
40+
app.get('/', (req, res) => {
41+
res.json({
42+
name: 'StackMemory API',
43+
version: '0.3.17',
44+
status: 'running',
45+
endpoints: ['/health', '/api/health', '/api/status', '/api/frames']
46+
});
47+
});
48+
49+
// Enhanced health check with service status
50+
app.get('/api/health', async (req, res) => {
51+
const checks: any = {
52+
server: 'ok',
53+
timestamp: new Date().toISOString()
54+
};
55+
56+
// Test PostgreSQL
57+
if (config.databaseUrl) {
58+
try {
59+
const pgClient = new Client({ connectionString: config.databaseUrl });
60+
await pgClient.connect();
61+
await pgClient.query('SELECT 1');
62+
await pgClient.end();
63+
checks.postgres = 'connected';
64+
} catch (error: any) {
65+
checks.postgres = 'error';
66+
checks.postgresError = error.message.substring(0, 100);
67+
}
68+
}
69+
70+
// Test Redis
71+
if (config.redisUrl) {
72+
try {
73+
const redisClient = createClient({ url: config.redisUrl });
74+
await redisClient.connect();
75+
await redisClient.ping();
76+
await redisClient.disconnect();
77+
checks.redis = 'connected';
78+
} catch (error: any) {
79+
checks.redis = 'error';
80+
checks.redisError = error.message.substring(0, 100);
81+
}
82+
}
83+
84+
const healthy = checks.postgres === 'connected' || checks.redis === 'connected';
85+
res.status(healthy ? 200 : 503).json(checks);
86+
});
87+
88+
// Status endpoint with detailed info
89+
app.get('/api/status', async (req, res) => {
90+
const status: any = {
91+
service: 'stackmemory',
92+
version: '0.3.17',
93+
environment: config.environment,
94+
storage: {}
95+
};
96+
97+
// PostgreSQL status
98+
if (config.databaseUrl) {
99+
try {
100+
const pgClient = new Client({ connectionString: config.databaseUrl });
101+
await pgClient.connect();
102+
103+
// Initialize frames table if needed
104+
await pgClient.query(`
105+
CREATE TABLE IF NOT EXISTS frames (
106+
frame_id TEXT PRIMARY KEY,
107+
run_id TEXT NOT NULL,
108+
project_id TEXT NOT NULL,
109+
parent_frame_id TEXT,
110+
depth INTEGER DEFAULT 0,
111+
type TEXT NOT NULL,
112+
name TEXT NOT NULL,
113+
state TEXT DEFAULT 'active',
114+
inputs JSONB DEFAULT '{}',
115+
outputs JSONB DEFAULT '{}',
116+
created_at TIMESTAMP DEFAULT NOW()
117+
)
118+
`);
119+
120+
const frameCount = await pgClient.query('SELECT COUNT(*) FROM frames');
121+
status.storage.postgres = {
122+
connected: true,
123+
frames: parseInt(frameCount.rows[0].count)
124+
};
125+
126+
await pgClient.end();
127+
} catch (error: any) {
128+
status.storage.postgres = {
129+
connected: false,
130+
error: error.message.substring(0, 100)
131+
};
132+
}
133+
}
134+
135+
// Redis status
136+
if (config.redisUrl) {
137+
try {
138+
const redisClient = createClient({ url: config.redisUrl });
139+
await redisClient.connect();
140+
141+
const keys = await redisClient.keys('*');
142+
status.storage.redis = {
143+
connected: true,
144+
keys: keys.length
145+
};
146+
147+
await redisClient.disconnect();
148+
} catch (error: any) {
149+
status.storage.redis = {
150+
connected: false,
151+
error: error.message.substring(0, 100)
152+
};
153+
}
154+
}
155+
156+
res.json(status);
157+
});
158+
159+
// Frames endpoint
160+
app.get('/api/frames', async (req, res) => {
161+
if (!config.databaseUrl) {
162+
return res.status(503).json({ error: 'Database not configured' });
163+
}
164+
165+
try {
166+
const pgClient = new Client({ connectionString: config.databaseUrl });
167+
await pgClient.connect();
168+
169+
const result = await pgClient.query(
170+
'SELECT * FROM frames ORDER BY created_at DESC LIMIT 10'
171+
);
172+
173+
await pgClient.end();
174+
res.json({
175+
count: result.rows.length,
176+
frames: result.rows
177+
});
178+
} catch (error: any) {
179+
res.status(500).json({
180+
error: 'Database error',
181+
message: error.message
182+
});
183+
}
184+
});
185+
186+
// Start server
187+
app.listen(config.port, '0.0.0.0', () => {
188+
console.log(`✅ Server running on port ${config.port}`);
189+
console.log(`📊 Database: ${config.databaseUrl ? 'configured' : 'not configured'}`);
190+
console.log(`💾 Redis: ${config.redisUrl ? 'configured' : 'not configured'}`);
191+
});
192+
}
193+
194+
startServer().catch(error => {
195+
console.error('Failed to start server:', error);
196+
process.exit(1);
197+
});

0 commit comments

Comments
 (0)