Skip to content

Repository files navigation

Roomer

Self-hosted workspace booking — desks, assets, and floor plans, fully under your control.

License: AGPL v3 Docker GitHub Actions Workflow Status


What is Roomer?

Roomer is a self-hosted platform for managing desk and asset reservations across offices, buildings, and floors. Upload floor plans, place bookable assets on a canvas, and let your team book space — without handing your occupancy data to a third-party SaaS.


Features

Floor plan editor Upload image, PDF or DXF floor plans and place assets on an interactive canvas
Desk & asset booking Book by date with live availability (available, booked, assigned, queued, restricted)
Permanent assignments Assign desks to users with primary/secondary ownership; bulk-assign via CSV
Zone management Group assets into colour-coded zones with optional conflict rules
Building & floor management Building admins and floor managers for delegated admin without full super-admin access
Queue & waitlist Users join a waitlist and are automatically promoted when a desk frees up
Recurring bookings Daily/weekly/monthly recurring booking rules that materialise the whole series up front
Availability windows Permanently-assigned desks can be opened for booking on specific date ranges
Floor subscriptions Users subscribe to a floor and are notified when matching space frees up
Analytics Utilisation and booking analytics for admins and building managers
Building leases Track building lease terms and costs (admin)
Departments & org chart Flat departments mapped from OIDC/SAML/LDAP/SCIM, plus an org hierarchy auto-inferred from each user's manager attribute — shown as a side-by-side people/department org chart
Bulk CSV import Import buildings, floors, zones, and assets in a single pass
Asset registry Track non-bookable inventory (laptops, monitors, etc.) alongside bookable space
Role-based access Super admin, building admin, floor manager, and user roles
Multi-provider login Local, LDAP, OIDC, and SAML — mix providers with a configurable default and URL overrides
SCIM 2.0 provisioning Automated user and group sync from Okta, Azure AD / Entra ID, OneLogin, etc.
Webhooks Outbound webhook endpoints with HMAC-SHA256 signed payloads, delivery log with retry, and 18 event types across booking, queue, asset, and user domains
Email notifications Booking confirmations and queue promotions via SMTP

Roomer-demo


Quick Start (Docker)

The fastest way to run Roomer is with the pre-built Docker images.

1. Create a .env file in the project root:

ROOMER_SESSION_SECRET=<run: openssl rand -hex 32>
ROOMER_ENCRYPTION_KEY=<run: openssl rand -hex 32>
APP_ORIGIN=http://localhost
ROOMER_COOKIE_SECURE=false
WEB_PORT=80
ROOMER_EMAIL_FROM=noreply@roomer.local

# Admin account credentials (see "Seeding" below)
ROOMER_SEED_ADMIN_EMAIL=admin@roomer.local
ROOMER_SEED_ADMIN_PASSWORD=changeme

2. Start everything:

docker compose up -d

The web app will be at http://localhost (or WEB_PORT if changed). The API container automatically runs prisma migrate deploy on startup, then seeds the admin account on first run.

Find the admin password: the seed step prints it to the API logs:

docker compose logs api | grep "seed"

If SEED_ADMIN_PASSWORD is not set, a random password is generated and logged there. Set it in your .env to use a known password.

3. (Optional) Seed demo buildings and desks:

Add SEED_DEMO_DATA=true to your .env (before first start, or re-run the seed manually):

docker compose exec api npx prisma db seed

This adds a sample building ("Acme HQ") with a floor, two zones, and six desks, plus a regular test user (user@roomer.local). Change all passwords after first login.

Docker Compose environment variables

All API variables use the ROOMER_ prefix in the root .env. Unprefixed names are still accepted by the API itself (for local dev), but Docker Compose and Kubernetes always inject the ROOMER_ form.

Variable Default Description
ROOMER_SESSION_SECRET — Required. 32+ character random string. Generate with openssl rand -hex 32.
ROOMER_ENCRYPTION_KEY — Required. 64 hex-char AES-256 key for encrypting auth provider secrets at rest. Generate with openssl rand -hex 32.
APP_ORIGIN http://localhost Public URL used for CORS_ORIGIN and APP_URL on the API container.
ROOMER_COOKIE_SECURE true Set to false only for plain HTTP testing.
WEB_PORT 8999 Host port for the web container.
ROOMER_EMAIL_FROM noreply@roomer.local Sender address for system emails.
ROOMER_SEED_ADMIN_EMAIL admin@roomer.local Email for the super-admin account created on first seed.
ROOMER_SEED_ADMIN_PASSWORD (random) Password for the super-admin account. If unset, a random password is generated and printed to the API container logs.
ROOMER_SEED_DEMO_DATA false Set to true to seed demo buildings, floors, desks and a test user on first start.
ROOMER_SEED_USER_PASSWORD (random) Password for the demo test user (user@roomer.local). Only used when ROOMER_SEED_DEMO_DATA=true.

Build from source

docker compose -f docker-compose.build.yaml up --build

Docker images

Image Description
c0dewhacker/roomer-api Multi-stage Node 24 Alpine. Runs migrations then starts Fastify.
c0dewhacker/roomer-web Multi-stage Vite build served by nginx 1.27. Proxies /api to the API.

Both images are built from the monorepo root so they can access packages/shared.


Stack

Layer Technology
API Fastify + TypeScript
Database PostgreSQL 18 + Prisma ORM
Frontend React 18 + Vite + Tailwind CSS + shadcn/ui
Canvas Konva (react-konva)
Job queue pg-boss (PostgreSQL-backed)
Monorepo pnpm workspaces + Turborepo

Development Setup

Prerequisites

  • Node.js v24+ (required by Prisma 7)
  • pnpm v10+ — npm install -g pnpm@10
  • Docker and Docker Compose

1. Clone and install

git clone https://github.com/c0dewhacker/Roomer.git roomer
cd roomer
pnpm install

2. Start infrastructure

docker compose up -d

This starts PostgreSQL and Mailpit (local SMTP catcher for dev email):

Service URL Notes
PostgreSQL localhost:5435 User/pass/db: roomer
Mailpit SMTP localhost:1025 Dev email relay
Mailpit Web http://localhost:8025 View outbound email

3. Configure the API

Create apps/api/.env:

# Required
DATABASE_URL=postgresql://roomer:roomer@localhost:5435/roomer
SESSION_SECRET=<openssl rand -hex 32>
# AES-256-GCM key for encrypting auth provider secrets (OIDC/LDAP/SAML)
ROOMER_ENCRYPTION_KEY=<openssl rand -hex 32>

# Defaults — override as needed
NODE_ENV=development
PORT=3001
CORS_ORIGIN=http://localhost:5173

# Email (matches Mailpit defaults above)
SMTP_HOST=localhost
SMTP_PORT=1025
SMTP_SECURE=false
EMAIL_FROM=noreply@roomer.local
APP_URL=http://localhost:5173

# Seed credentials — set these before running db:seed so you know the password.
# If SEED_ADMIN_PASSWORD is omitted, a random password is generated and printed to stdout.
SEED_ADMIN_EMAIL=admin@roomer.local
SEED_ADMIN_PASSWORD=admin123
SEED_DEMO_DATA=true

All variables also accept the ROOMER_ prefix (e.g. ROOMER_DATABASE_URL). ROOMER_<NAME> takes precedence when both are set — Docker Compose and Kubernetes always use the prefixed form. For local dev the unprefixed names are fine.

4. Migrate and seed

pnpm --filter api db:migrate   # apply migrations
pnpm --filter api db:seed      # seed demo data

With the .env values above, the seed creates:

Resource Value
Admin login admin@roomer.local / admin123 (from your .env)
Organisation Acme Corp (renamed when SEED_DEMO_DATA=true)
Building Acme HQ with Ground Floor — 6 sample desks across two zones
Test user user@roomer.local (password from SEED_USER_PASSWORD, or logged if unset)

Without SEED_DEMO_DATA=true, only the admin account and a blank organisation ("My Organisation") are created.

5. Start dev servers

pnpm dev                      # both apps via Turborepo
# or individually:
pnpm --filter api dev          # API  → http://localhost:3001
pnpm --filter web dev          # Web  → http://localhost:5173

Open http://localhost:5173 and sign in with admin@roomer.local / admin123.


Production Deployment

The Docker images are production-ready as-is. Key checklist:

  • Set COOKIE_SECURE=true and serve over HTTPS
  • Set APP_ORIGIN to your public domain (e.g. https://roomer.example.com)
  • Set API_PUBLIC_URL to your public API URL if different from APP_ORIGIN (used for SCIM endpoint links)
  • Use a managed PostgreSQL instance by overriding DATABASE_URL in the API environment
  • Mount a persistent volume at /app/uploads for floor plan storage (already in docker-compose.yml)
  • For horizontal API scaling, point FILE_STORAGE_PATH at shared network storage rather than a local volume

See the Backup and Recovery wiki page for backing up the database and uploads volume, and a restore runbook.

Kubernetes (Helm)

A Helm chart is included at charts/roomer/. Fetch sub-chart dependencies, then install:

helm dependency update ./charts/roomer
helm install roomer ./charts/roomer \
  --namespace roomer --create-namespace \
  --set ingress.host=roomer.example.com \
  --set secrets.sessionSecret=$(openssl rand -hex 32) \
  --set secrets.encryptionKey=$(openssl rand -hex 32) \
  --set secrets.databaseUrl="postgresql://user:pass@host:5432/roomer"

TLS with cert-manager (Let's Encrypt):

helm install roomer ./charts/roomer \
  --namespace roomer --create-namespace \
  --set ingress.host=roomer.example.com \
  --set ingress.tls.enabled=true \
  --set ingress.tls.certManager.enabled=true \
  --set ingress.tls.certManager.issuerName=letsencrypt-prod \
  --set secrets.sessionSecret=$(openssl rand -hex 32) \
  --set secrets.encryptionKey=$(openssl rand -hex 32) \
  --set secrets.databaseUrl="postgresql://user:pass@host:5432/roomer"

See the Kubernetes Deployment wiki page for the full values reference and upgrade instructions.


API Reference

The REST API runs on port 3001 by default.

  • Swagger UI: http://localhost:3001/docs (enabled in development; set SWAGGER_ENABLED=true to enable in production)
  • All routes are prefixed /api/v1
Prefix Description
/api/v1/auth Login, logout, session, enterprise SSO (OIDC/SAML/LDAP)
/api/v1/buildings Buildings CRUD, managers, access groups
/api/v1/floors Floors, zones, floor plan upload, availability
/api/v1/assets Asset registry, bookable desks, user assignments, bulk assignment CSV
/api/v1/bookings Booking lifecycle
/api/v1/queue Waitlist management
/api/v1/import/bulk Global CSV bulk import
/api/v1/users User management
/api/v1/groups Group management
/api/v1/settings Organisation settings, auth provider config, SCIM provisioning
/api/v1/webhooks Webhook endpoint management, delivery log
/scim/v2 SCIM 2.0 provisioning (separate prefix, bearer token auth)
/health/live Liveness probe — always 200 while the process is running
/health/ready Readiness probe — 200 when the DB is reachable, 503 otherwise
/metrics Prometheus metrics (enabled via ROOMER_METRICS_ENABLED=true)

Programmatic / BI tool access

There's no separate API-key subsystem — the existing session auth already covers headless/automated access:

  1. Create a dedicated local user for the integration (Settings → Users), not a real person's account, with a strong generated password.
  2. POST /api/v1/auth/login with that user's email/password. The response sets the session JWT as an httpOnly cookie — any HTTP client library (Power BI's Web connector, Python requests, Postman, etc.) can still read it off the raw Set-Cookie response header even though browser JavaScript can't.
  3. Reuse that cookie on subsequent requests (most HTTP client libraries handle cookie jars automatically), or extract the JWT from it and send Authorization: Bearer <token> instead if the tool doesn't manage cookies — set ALLOW_BEARER_AUTH=true for this (defaults to true in development, must be explicitly enabled in production).
  4. The token expires after a fixed session ceiling — a scheduled BI refresh just re-runs step 2 each run rather than needing a long-lived credential, which is how most scheduled BI refreshes already work.

Scope the service-account user's role/resource-roles the same way you would for a real admin — there's no separate read-only surface, so grant the least privilege the integration actually needs (e.g. a BUILDING_ADMIN resource role scoped to one building for a single-building reporting dashboard, rather than SUPER_ADMIN).


Configuration Reference

All API environment variables. Every variable accepts a ROOMER_ prefix (e.g. ROOMER_DATABASE_URL); the prefixed form takes precedence when both are set. Docker Compose and Kubernetes always inject the ROOMER_ form.

Variable Default Description
DATABASE_URL — PostgreSQL connection string (required)
SESSION_SECRET — Cookie signing secret, min 32 chars (required)
ROOMER_ENCRYPTION_KEY — 64-char hex AES-256 key for encrypting auth provider secrets at rest (required). Generate with openssl rand -hex 32.
PORT 3001 API listen port
HOST 0.0.0.0 API bind address
CORS_ORIGIN http://localhost:5173 Allowed frontend origin
COOKIE_SECURE false in dev Require HTTPS for session cookies
TRUST_PROXY loopback,uniquelocal in prod, false in dev Peers trusted to set X-Forwarded-For, which resolves request.ip (rate-limit keying, audit-log IPs). false = trust nothing; otherwise a comma-separated list of proxy IPs/CIDRs, or the presets loopback / linklocal / uniquelocal. true and hop counts are rejected — true trusts the whole chain (clients could spoof their own IP), and fastify ≥5.12.1 no longer supports hop counts
ALLOW_BEARER_AUTH true in dev Accept Authorization: Bearer tokens
SWAGGER_ENABLED true in dev Expose Swagger UI at /docs
ROOMER_METRICS_ENABLED false Set to true to expose a Prometheus /metrics endpoint. Unauthenticated unless METRICS_TOKEN is set — protect at the network/ingress level.
METRICS_TOKEN — Optional bearer token for /metrics. When set, scrapers must send Authorization: Bearer <token>.
WEBHOOK_ALLOW_PRIVATE false Allow webhook delivery to private/RFC1918 addresses (internal integrations). Loopback and link-local (incl. cloud metadata) remain blocked regardless.
FILE_STORAGE_PATH ./uploads Directory for floor plan images
MAX_FILE_SIZE_MB 20 Maximum upload size
API_PUBLIC_URL http://localhost:3001 Public URL of the API — shown in the SCIM settings panel as the endpoint base URL
SMTP_HOST localhost Outbound email relay host
SMTP_PORT 1025 Outbound email relay port
SMTP_SECURE false Use TLS for SMTP
SMTP_USER — SMTP username (if required)
SMTP_PASS — SMTP password (if required)
EMAIL_FROM noreply@roomer.local Sender address for system emails

Email/SMTP can also be configured in the admin UI (Settings → Email). Any of the SMTP_* / EMAIL_FROM variables above, when set, override the UI value at runtime (captured at startup) and appear locked in the form. | APP_URL | http://localhost:5173 | Public URL used in email links |

Upgrading to 1.1.0 — TRUST_PROXY=true is no longer accepted

If your deployment sets TRUST_PROXY=true, change it before upgrading — the API now refuses to start with that value.

Use the peers you actually trust to set X-Forwarded-For. Behind the bundled nginx (Docker Compose or the Helm chart), that is:

ROOMER_TRUST_PROXY=loopback,uniquelocal

The shipped docker-compose.yml, docker-compose.build.yaml and Helm chart are already updated, so stock deployments need no action — this only affects a hand-written .env or custom manifests.

Why it's a hard failure rather than a silent default. true told the server to trust the entire X-Forwarded-For chain, which made request.ip the left-most entry — a value the client sends. Since nginx appends the real peer to whatever the client supplied, anyone could set their own request.ip, mint a fresh rate-limit bucket per request, and forge the IP recorded on every audit-log row. The correct replacement depends on your own topology, and guessing it wrong fails quietly: everyone collapses onto a single bucket keyed by the proxy's address, so the global 300 req/min limit throttles the whole deployment at once. Refusing to boot is the louder, safer failure.

Hop counts (TRUST_PROXY=1) are rejected for the same reason — fastify removed numeric hop-count trust in 5.12.1, where a number now means "trust nothing".


Roles

Role Scope Permissions
SUPER_ADMIN Global Full access to all resources and settings
BUILDING_ADMIN Per building Superset of floor manager for every floor in the building
FLOOR_MANAGER Per floor Manage zones, assets, and bookings on assigned floors
USER Global Book available assets, manage own bookings

Available Scripts

Run from the monorepo root:

Command Description
pnpm dev Start all apps in development mode
pnpm build Build all apps for production
pnpm lint Lint all packages
pnpm --filter api db:migrate Run pending Prisma migrations
pnpm --filter api db:seed Seed demo data
pnpm --filter api db:studio Open Prisma Studio (database browser)

Project Structure

roomer/
├── apps/
│   ├── api/              # Fastify REST API
│   │   ├── prisma/       # Schema, migrations, seed
│   │   └── src/
│   │       ├── routes/
│   │       ├── middleware/
│   │       └── lib/
│   └── web/              # React SPA
│       └── src/
│           ├── components/
│           ├── pages/
│           ├── hooks/
│           └── stores/
├── packages/
│   └── shared/           # Shared types and Zod schemas
├── docker-compose.yml        # Pull pre-built images
├── docker-compose.build.yaml # Build from source
└── turbo.json

License

Roomer is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0).

In short: you can use, modify, self-host, and distribute Roomer freely, including commercially. If you modify Roomer and make it available to others over a network (e.g. as a hosted service), you must release your modified source under the same license.

See the LICENSE file for the full terms.

About

Roomer is a self-hosted platform for managing desk and asset reservations across offices, buildings, and floors. Upload floor plans, place bookable assets on a canvas, and let your team book space — without handing your occupancy data to a third-party SaaS.

Topics

Resources

Stars

11 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages