Deployment

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 Overview

PlatformTypeAPIStaticWebSocketFree Tier
RailwayPersistent server$5 trial credit
RenderPersistent server✓ (750h/month)
VercelServerless + CDN✓*

* Vercel runs API routes as serverless functions. In-memory state (rate limiter, event bus) resets between invocations. WebSocket is not supported.

app.handle() — Serverless Adapter

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.

Railway

Best for: Full SpaceNode apps — API, WebSocket, events, static files. Persistent Node.js process, zero cold starts.

API Application

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)
Important: Always use 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.

Static Site

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 loginrailway initrailway up.

Render

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).

API Application

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://...

Static Site

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.

Vercel

Best for: Static sites (CDN) and simple stateless APIs. Not suitable for WebSocket or in-memory state.

API Application

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)
Why 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
Serverless limitations: Each request runs in an isolated function. In-memory state (rateLimit, Event Bus listeners) does not persist between invocations. Use external stores (Redis, database) for state that must survive across requests.
WebSocket on Vercel: Vercel serverless functions are request → response only. WebSocket requires a persistent TCP connection, which serverless cannot maintain. This applies to all frameworks — Express, Fastify, SpaceNode. If you see Express "WebSocket" demos on Vercel, they use external real-time services (Pusher, Ably, Supabase Realtime) or polling fallbacks, not native WebSocket. For true WebSocket, deploy on Railway or Render.

Static Site (Pure CDN)

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.

API + Static (Hybrid)

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)
Static files on Vercel: Don't use 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).

Which Platform to Choose?

ScenarioRecommendedWhy
Full API + WebSocket + EventsRailway / RenderPersistent server, in-memory state works
Simple stateless REST APIVercel / RenderFree tiers, auto-scaling
Static site / documentationVercelGlobal CDN, instant deploys, free
API + static in one appRailway / RenderSingle process serves both
Free persistent hostingRender750h/month free (enough for 1 service)

Environment Variables

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)
PlatformHow to set
RailwayDashboard → Variables, or railway variables set KEY=value
RenderDashboard → Environment
VercelDashboard → Settings → Environment Variables, or vercel env add

Docker (Any Platform)

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.