SpaceNode
SpaceNode apps can be deployed anywhere Node.js runs. This guide covers three popular platforms with step-by-step instructions for API applications and static sites.
| Platform | Type | API | Static | WebSocket | Free Tier |
|---|---|---|---|---|---|
| Railway | Persistent server | ✓ | ✓ | ✓ | $5 trial credit |
| Render | Persistent server | ✓ | ✓ | ✓ | ✓ (750h/month) |
| Vercel | Serverless + CDN | ✓* | ✓ | ✗ | ✓ |
* Vercel runs API routes as serverless functions. In-memory state (rate limiter, event bus) resets between invocations. WebSocket is not supported.
SpaceNode exposes a public app.handle(req, res) method — the raw request handler. Use it on serverless platforms instead of app.listen():
// Traditional server:
app.listen(3000)
// Serverless (Vercel, AWS Lambda, etc.):
export default (req, res) => app.handle(req, res)
Both approaches use the same app instance — your routes, guards, services, and modules work identically.
Best for: Full SpaceNode apps — API, WebSocket, events, static files. Persistent Node.js process, zero cold starts.
1. Project structure:
my-api/
app.js
package.json
modules/
auth/
module.js
auth.controller.js
auth.service.js
2. package.json:
{
"name": "my-api",
"type": "module",
"scripts": {
"start": "node app.js"
},
"dependencies": {
"spacenode": "^1.0.0"
},
"engines": { "node": ">=18" }
}
3. app.js:
import { createApp } from 'SpaceNode'
const app = await createApp()
const port = process.env.PORT || 3000
app.listen(port)
process.env.PORT. Railway assigns a dynamic port — hardcoding a port will cause deployment to fail.
4. Deploy:
# Install Railway CLI
npm i -g @railway/cli
# Login & init
railway login
railway init
# Deploy
railway up
Railway auto-detects Node.js, runs npm install + npm start, and assigns a public URL. Done.
1. Project structure:
my-site/
app.js
package.json
public/
index.html
style.css
404.html
2. app.js:
import { createApp } from 'SpaceNode'
const app = await createApp({
static: './public',
spa: false, // true for SPA, false for multi-page
})
const port = process.env.PORT || 3000
app.listen(port)
3. Deploy: same steps — railway login → railway init → railway up.
Best for: Free hosting with persistent server. Supports API, WebSocket, and static files. Spins down after 15 min of inactivity on free tier (~30s cold start).
1. package.json — same as Railway (must have "start": "node app.js").
2. app.js — same as Railway (use process.env.PORT).
3. Deploy via Dashboard:
1. Push code to GitHub/GitLab
2. Go to https://dashboard.render.com → New → Web Service
3. Connect your repository
4. Settings:
Runtime: Node
Build Command: npm install
Start Command: node app.js
5. Click "Create Web Service"
4. Environment variables — add in Render Dashboard → Environment:
NODE_ENV=production
DATABASE_URL=mongodb+srv://...
Render has a dedicated Static Site type — serves files from a directory via CDN, no Node.js process needed.
1. Deploy via Dashboard:
1. Push code to GitHub/GitLab
2. Go to Render Dashboard → New → Static Site
3. Connect your repository
4. Settings:
Publish Directory: public
5. Click "Create Static Site"
Render serves files directly from public/. No SpaceNode server needed — pure CDN.
Custom 404 on Render: Render auto-detects 404.html in the publish directory and serves it for missing paths.
Alternative (as Web Service): If you need SpaceNode features (clean URLs, custom headers, API + static combo), deploy as a Web Service with the same app.js as Railway.
Best for: Static sites (CDN) and simple stateless APIs. Not suitable for WebSocket or in-memory state.
Vercel runs each request as a serverless function. It does not call app.listen() — instead, it imports your file and calls the exported function.
1. Project structure:
my-api/
api/
index.js ← serverless entry point
modules/
auth/
module.js
auth.controller.js
auth.service.js
vercel.json
package.json
2. api/index.js — the serverless adapter:
import { createApp } from 'SpaceNode'
const app = await createApp({
baseUrl: import.meta.url, // resolve paths from this file
modulesDir: '../modules'
})
export default (req, res) => app.handle(req, res)
baseUrl? On Vercel, process.argv[1] points to the platform's bootstrap runtime, not your file. Setting baseUrl: import.meta.url ensures all relative paths (modulesDir, static) resolve from your file's directory. This is recommended for any serverless or non-standard environment.
3. vercel.json — route all requests to the handler:
{
"rewrites": [
{ "source": "/(.*)", "destination": "/api" }
]
}
4. package.json:
{
"name": "my-api",
"type": "module",
"dependencies": {
"spacenode": "^1.0.0"
}
}
5. Deploy:
# Install Vercel CLI
npm i -g vercel
# Deploy
vercel
rateLimit, Event Bus listeners) does not persist between invocations. Use external stores (Redis, database) for state that must survive across requests.
For pure static sites, Vercel serves files directly from a directory via its global CDN — no serverless functions needed.
1. Project structure:
my-site/
public/
index.html
style.css
404.html ← Vercel auto-detects this
vercel.json
2. vercel.json:
{
"outputDirectory": "public"
}
3. Deploy:
vercel
Vercel serves all files from public/ via its global CDN. It auto-detects 404.html and shows it for missing paths. No Node.js process needed.
To serve both API routes and static assets from a single Vercel project, put static files in the root public/ folder. Vercel serves files from public/ automatically via CDN — they take priority over serverless functions. Only non‑static requests hit your API function.
1. Project structure:
my-app/
api/
index.js ← serverless API handler
public/
index.html ← served via CDN automatically
style.css
logo.png
modules/
todo/
module.js
todo.controller.js
vercel.json
package.json
2. vercel.json — only route non-static requests to the API:
{
"rewrites": [
{ "source": "/api/(.*)", "destination": "/api" }
]
}
3. api/index.js:
import { createApp } from 'SpaceNode'
const app = await createApp({
baseUrl: import.meta.url, // required on serverless
modulesDir: '../modules'
// No static config — Vercel CDN handles static files
})
export default (req, res) => app.handle(req, res)
config.static on Vercel. Vercel's CDN serves files from public/ automatically — faster (edge network, cached globally) and doesn't consume serverless function invocations. Use config.static only on persistent servers (Railway, Render).
| Scenario | Recommended | Why |
|---|---|---|
| Full API + WebSocket + Events | Railway / Render | Persistent server, in-memory state works |
| Simple stateless REST API | Vercel / Render | Free tiers, auto-scaling |
| Static site / documentation | Vercel | Global CDN, instant deploys, free |
| API + static in one app | Railway / Render | Single process serves both |
| Free persistent hosting | Render | 750h/month free (enough for 1 service) |
All platforms support environment variables. Use them for secrets and configuration:
import mongoose from 'mongoose'
import { createApp } from 'SpaceNode'
// 1. Connect to database BEFORE createApp
await mongoose.connect(process.env.MONGO_URI)
// 2. Create app — modules will use the active connection
const app = await createApp()
const port = process.env.PORT || 3000
app.listen(port)
| Platform | How to set |
|---|---|
| Railway | Dashboard → Variables, or railway variables set KEY=value |
| Render | Dashboard → Environment |
| Vercel | Dashboard → Settings → Environment Variables, or vercel env add |
SpaceNode works in Docker with zero extra config:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]
Works on Railway, Render, Fly.io, DigitalOcean, AWS ECS — any platform that supports Docker.