sg-luma-events
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data β all from the browser, and deployed in milliseconds.
This guide contains all the code and configuration files you need to deploy your own Luma CORS proxy to Railway.
Option A: Deno (Recommended)
- Modern TypeScript runtime
- No package management needed
- Files needed:
deno-server.ts+railway.toml
Option B: Node.js
- Traditional Node.js with Express
- Standard npm ecosystem
- Files needed:
node-server.js+package.json
File: deno-server.ts
import { serve } from "https://deno.land/std@0.208.0/http/server.ts";
/**
* Railway-ready Deno server for Luma API CORS Proxy
*
* This proxy solves CORS issues when accessing Luma API from frontend applications
*/
async function handleRequest(req: Request): Promise<Response> {
// Handle preflight OPTIONS request
if (req.method === 'OPTIONS') {
return new Response(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Max-Age': '86400',
},
});
}
// Only allow GET requests
if (req.method !== 'GET') {
return new Response('Method not allowed', {
status: 405,
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/plain',
},
});
}
try {
// Extract query parameters from the incoming request
const url = new URL(req.url);
const searchParams = url.searchParams;
// Check if we should filter to next 3 events
const limitNext3 = searchParams.get('next3') === 'true';
// Build the Luma API URL with query parameters
const lumaUrl = new URL('https://api.lu.ma/public/v1/calendar/list-events');
// If we're filtering for next 3, optimize the API call
if (limitNext3) {
// Remove next3 from params to forward to Luma API
searchParams.delete('next3');
// Use correct Luma API parameter names from the documentation
lumaUrl.searchParams.set('pagination_limit', '100');
// Try to get events from a reasonable time range
// Use 'after' parameter with current date to get future events
const today = new Date();
const todayISO = today.toISOString();
lumaUrl.searchParams.set('after', todayISO);
// Sort by start date ascending to get earliest upcoming events first
lumaUrl.searchParams.set('sort_column', 'start_at');
lumaUrl.searchParams.set('sort_direction', 'asc');
}
// Forward all remaining query parameters to the Luma API
for (const [key, value] of searchParams.entries()) {
lumaUrl.searchParams.set(key, value);
}
// Get API key from environment variable (more secure) or fallback to hardcoded
const apiKey = Deno.env.get('LUMA_API_KEY') || 'secret-FMPoZlwNgVJ0qkCSn2EIHpTUA';
// Make the request to Luma API with the API key
const lumaResponse = await fetch(lumaUrl.toString(), {
method: 'GET',
headers: {
'x-luma-api-key': apiKey,
'Content-Type': 'application/json',
},
});
// Get the response data
const data = await lumaResponse.json();
let filteredData = data;
if (limitNext3 && data.entries && Array.isArray(data.entries)) {
const now = new Date();
// First, try to find upcoming events
let upcomingEvents = data.entries
.filter(entry => {
const eventStartTime = new Date(entry.event.start_at);
return eventStartTime > now; // Only future events
})
.sort((a, b) => {
const dateA = new Date(a.event.start_at);
const dateB = new Date(b.event.start_at);
return dateA - dateB; // Sort by date ascending (earliest first)
});
// If we have upcoming events, use them
if (upcomingEvents.length > 0) {
upcomingEvents = upcomingEvents.slice(0, 3);
filteredData = {
...data,
entries: upcomingEvents,
has_more: upcomingEvents.length === 3 && (data.entries.length > upcomingEvents.length || data.has_more)
};
} else {
// No upcoming events, fall back to most recent past events
const recentPastEvents = data.entries
.filter(entry => {
const eventStartTime = new Date(entry.event.start_at);
return eventStartTime <= now; // Only past events
})
.sort((a, b) => {
const dateA = new Date(a.event.start_at);
const dateB = new Date(b.event.start_at);
return dateB - dateA; // Sort by date descending (most recent first)
})
.slice(0, 3);
filteredData = {
...data,
entries: recentPastEvents,
has_more: false, // Since we're showing past events as fallback
_fallback: 'recent_past_events' // Indicator that we fell back to past events
};
}
}
// Return the data with CORS headers
return new Response(JSON.stringify(filteredData), {
status: lumaResponse.status,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Content-Type': 'application/json',
},
});
} catch (error) {
console.error('Proxy error:', error);
return new Response(JSON.stringify({
error: 'Failed to fetch from Luma API',
message: error instanceof Error ? error.message : 'Unknown error'
}), {
status: 500,
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
},
});
}
}
// Start the server
const port = parseInt(Deno.env.get("PORT") || "8000");
console.log(`π Luma CORS Proxy running on port ${port}`);
console.log(`π‘ Ready to proxy requests to Luma API`);
serve(handleRequest, { port });
File: railway.toml
[build] builder = "nixpacks" [deploy] startCommand = "deno run --allow-net --allow-env deno-server.ts" [environments.production] variables = {} [environments.staging] variables = {}
File: node-server.js
const express = require('express');
const app = express();
/**
* Railway-ready Node.js server for Luma API CORS Proxy
*
* This proxy solves CORS issues when accessing Luma API from frontend applications
*/
// CORS middleware
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
res.header('Access-Control-Max-Age', '86400');
return res.status(200).end();
}
next();
});
// Main proxy endpoint
app.get('*', async (req, res) => {
try {
// Only allow GET requests (OPTIONS handled above)
if (req.method !== 'GET') {
return res.status(405).send('Method not allowed');
}
// Extract query parameters from the incoming request
const searchParams = new URLSearchParams(req.query);
// Check if we should filter to next 3 events
const limitNext3 = searchParams.get('next3') === 'true';
// Build the Luma API URL with query parameters
const lumaUrl = new URL('https://api.lu.ma/public/v1/calendar/list-events');
// If we're filtering for next 3, optimize the API call
if (limitNext3) {
// Remove next3 from params to forward to Luma API
searchParams.delete('next3');
// Use correct Luma API parameter names from the documentation
lumaUrl.searchParams.set('pagination_limit', '100');
// Try to get events from a reasonable time range
// Use 'after' parameter with current date to get future events
const today = new Date();
const todayISO = today.toISOString();
lumaUrl.searchParams.set('after', todayISO);
// Sort by start date ascending to get earliest upcoming events first
lumaUrl.searchParams.set('sort_column', 'start_at');
lumaUrl.searchParams.set('sort_direction', 'asc');
}
// Forward all remaining query parameters to the Luma API
for (const [key, value] of searchParams.entries()) {
lumaUrl.searchParams.set(key, value);
}
// Get API key from environment variable (more secure) or fallback to hardcoded
const apiKey = process.env.LUMA_API_KEY || 'secret-FMPoZlwNgVJ0qkCSn2EIHpTUA';
// Make the request to Luma API with the API key
const lumaResponse = await fetch(lumaUrl.toString(), {
method: 'GET',
headers: {
'x-luma-api-key': apiKey,
'Content-Type': 'application/json',
},
});
// Get the response data
const data = await lumaResponse.json();
let filteredData = data;
if (limitNext3 && data.entries && Array.isArray(data.entries)) {
const now = new Date();
// First, try to find upcoming events
let upcomingEvents = data.entries
.filter(entry => {
const eventStartTime = new Date(entry.event.start_at);
return eventStartTime > now; // Only future events
})
.sort((a, b) => {
const dateA = new Date(a.event.start_at);
const dateB = new Date(b.event.start_at);
return dateA.getTime() - dateB.getTime(); // Sort by date ascending (earliest first)
});
// If we have upcoming events, use them
if (upcomingEvents.length > 0) {
upcomingEvents = upcomingEvents.slice(0, 3);
filteredData = {
...data,
entries: upcomingEvents,
has_more: upcomingEvents.length === 3 && (data.entries.length > upcomingEvents.length || data.has_more)
};
} else {
// No upcoming events, fall back to most recent past events
const recentPastEvents = data.entries
.filter(entry => {
const eventStartTime = new Date(entry.event.start_at);
return eventStartTime <= now; // Only past events
})
.sort((a, b) => {
const dateA = new Date(a.event.start_at);
const dateB = new Date(b.event.start_at);
return dateB.getTime() - dateA.getTime(); // Sort by date descending (most recent first)
})
.slice(0, 3);
filteredData = {
...data,
entries: recentPastEvents,
has_more: false, // Since we're showing past events as fallback
_fallback: 'recent_past_events' // Indicator that we fell back to past events
};
}
}
// Return the data with proper status code
res.status(lumaResponse.status).json(filteredData);
} catch (error) {
console.error('Proxy error:', error);
res.status(500).json({
error: 'Failed to fetch from Luma API',
message: error instanceof Error ? error.message : 'Unknown error'
});
}
});
// Health check endpoint
app.get('/health', (req, res) => {
res.json({ status: 'ok', service: 'luma-cors-proxy' });
});
// Start the server
const port = process.env.PORT || 8000;
app.listen(port, () => {
console.log(`π Luma CORS Proxy running on port ${port}`);
console.log(`π‘ Ready to proxy requests to Luma API`);
console.log(`π Health check: http://localhost:${port}/health`);
});
File: package.json
{ "name": "luma-cors-proxy", "version": "1.0.0", "description": "CORS proxy for Luma API - Railway deployment", "main": "node-server.js", "scripts": { "start": "node node-server.js", "dev": "node node-server.js" }, "dependencies": { "express": "^4.18.2" }, "engines": { "node": ">=18" }, "keywords": ["cors", "proxy", "luma", "api", "railway"], "author": "Your Name", "license": "MIT" }
# Create a new repository with your chosen files mkdir luma-cors-proxy cd luma-cors-proxy # For Deno deployment (recommended): # Copy the deno-server.ts and railway.toml content above into files # OR for Node.js deployment: # Copy the node-server.js and package.json content above into files # Initialize git and push git init git add . git commit -m "Initial commit - Luma CORS Proxy for Railway" git remote add origin https://github.com/yourusername/luma-cors-proxy.git git push -u origin main
- Go to railway.app
- Sign up/Login (can use GitHub)
- Click "Deploy from GitHub repo"
- Select your repository
- Railway auto-detects your runtime and deploys!
In Railway dashboard:
- Go to your project
- Click "Variables" tab
- Add:
LUMA_API_KEY=secret-FMPoZlwNgVJ0qkCSn2EIHpTUA
After deployment, Railway gives you a URL like:
https://luma-cors-proxy-production.up.railway.app
# Test basic functionality curl https://your-app.up.railway.app # Test next3 filter curl "https://your-app.up.railway.app?next3=true" # Test with calendar ID curl "https://your-app.up.railway.app?calendar_id=cal-0pgAb0xbjL529WF&next3=true"
Replace your API calls with the new Railway URL:
// Use your new Railway proxy
fetch('https://your-app.up.railway.app')
.then(response => response.json())
.then(data => {
console.log('Luma events:', data);
// Use your event data here
});
Your CORS proxy is now running on Railway with:
- β Serverless architecture
- β Custom domain support
- β Auto-scaling
- β Environment variables
- β Git-based deployments
- β Free tier available
Deployment fails?
- Check Railway logs in dashboard
- Verify file names match the runtime choice
- Ensure all files are committed to git
Proxy not working?
- Test the health endpoint:
/health(Node.js version) - Check environment variables are set
- Verify the proxy returns expected JSON data
CORS issues?
- Verify the proxy is returning proper CORS headers
- Test with a simple curl command first
- Check browser network tab for error details
- Custom Domain: Add your own domain in Railway dashboard
- Monitoring: Set up Railway's built-in monitoring
- Scaling: Configure auto-scaling if needed
- Security: Consider rate limiting for production use
Your Luma CORS proxy is ready to handle all your event data needs! π―