Skip to content

Chapter247IND/poc-geofence-socket

Repository files navigation

Multi-tenant Next.js 14 Demo

A focused App Router demo that shows three distinct backend capabilities working together in a single project — no fluff, just correct implementations.

Feature What it does
Multi-tenant middleware Reads the subdomain from Host, resolves it against a mock Redis store, injects x-tenant-id / x-tenant-name headers, and returns 404 for unknown tenants
PostGIS geofence POST /api/geofence accepts [lat, lng] and answers inside/outside using a raw ST_Contains query via Prisma $queryRaw
Socket.io realtime A shared counter that broadcasts updates to every connected browser tab in under 500 ms

Prerequisites

Requirement Version
Node.js 18 or later
PostgreSQL 14 + with the PostGIS extension enabled

No Docker required. Connect to any local or remote PostGIS-enabled Postgres instance.


Quick start

# 1. Clone and install
git clone <repo-url>
cd multi-tenant
npm install

# 2. Configure the database URL
cp .env.example .env
# Edit .env and set DATABASE_URL to your Postgres connection string

# 3. Create the database and enable PostGIS (run inside psql)
#   CREATE DATABASE geofence_demo;
#   \c geofence_demo
#   CREATE EXTENSION IF NOT EXISTS postgis;

# 4. Push the Prisma schema
npm run db:push

# 5. Start the dev server
npm run dev

Open http://localhost:3000.

Important: always start with npm run dev, not next dev.
Socket.io needs to attach to the same HTTP server as Next.js — that custom wiring lives in server.ts.

When the server starts you will see:

> Ready on http://localhost:3000
[db] Connected ✓
[db] PostGIS: 3.x ...

If the database is unreachable you will see [db] Not connected ✗ with the error message — the rest of the app still works, only the geofence API will fail.


Feature details

1 · Multi-tenant middleware

The middleware runs on every request before any route handler.

Flow:

  1. Reads the Host header (e.g. acme.localhost:3000)
  2. Strips the port and root host to extract the subdomain (acme)
  3. Looks up the slug in the mock Redis store (lib/redis-mock.ts)
  4. Known tenant → injects x-tenant-id and x-tenant-name into the request headers and continues
  5. Unknown tenant → returns 404 Unknown tenant immediately

Subdomains on localhost

Modern browsers resolve *.localhost to 127.0.0.1 — no /etc/hosts changes needed.

URL Expected result
http://acme.localhost:3000/tenant 200 — tenant headers shown
http://globex.localhost:3000/tenant 200 — tenant headers shown
http://unknown.localhost:3000/tenant 404 Unknown tenant
http://localhost:3000/tenant 200 — no tenant headers (apex host)

Known tenants: acme, globex (edit lib/redis-mock.ts to add more).

Test with curl:

# Returns { tenantId, tenantName }
curl -s http://acme.localhost:3000/api/tenant | jq

# Returns 404
curl -si http://unknown.localhost:3000/api/tenant

2 · PostGIS geofence

Endpoint: POST /api/geofence
Body: [lat, lng] — a JSON array of two numbers
Response: { inside: boolean, lat, lng }

The polygon is a hardcoded rectangle around downtown San Francisco (see lib/geofence-constants.ts). The check runs entirely inside PostgreSQL using ST_Contains.

SELECT ST_Contains(
  ST_GeomFromText('<WKT polygon>', 4326),
  ST_SetSRID(ST_MakePoint(<lng>, <lat>), 4326)
) AS inside

Test with curl:

# Inside the fence (SF downtown)
curl -s -X POST http://localhost:3000/api/geofence \
  -H 'Content-Type: application/json' \
  -d '[37.78, -122.40]' | jq
# → { "inside": true, "lat": 37.78, "lng": -122.4 }

# Outside the fence (Oakland)
curl -s -X POST http://localhost:3000/api/geofence \
  -H 'Content-Type: application/json' \
  -d '[37.80, -122.27]' | jq
# → { "inside": false, ... }

UI: http://localhost:3000/geofence — includes quick presets and coordinate inputs.


3 · Real-time counter (Socket.io)

UI: http://localhost:3000/realtime

  1. Open the page in two browser tabs
  2. Click Increment in one tab — the counter updates in both tabs instantly
  3. Click Reset — all tabs return to 0

The round-trip latency is displayed after each action.

Socket events:

Event (client → server) Effect
increment counter += 1, broadcast counter to all sockets
counter-reset counter = 0, broadcast counter to all sockets
Event (server → client) Payload
counter number — current counter value

Project layout

.
├── server.ts                  # Custom HTTP server: Next.js + Socket.io
├── middleware.ts               # Subdomain extraction → mock Redis → headers / 404
│
├── lib/
│   ├── redis-mock.ts           # In-memory tenant store (stand-in for Redis)
│   ├── subdomain.ts            # Host → subdomain parser
│   ├── geofence.ts             # isPointInsideFence() via Prisma $queryRaw
│   ├── geofence-constants.ts   # Hardcoded polygon WKT + bounds
│   ├── prisma.ts               # Prisma client singleton
│   ├── counter-socket.ts       # Socket.io counter event handlers
│   └── check-db.ts             # Startup DB connection + PostGIS check
│
├── app/
│   ├── page.tsx                # Home — feature overview
│   ├── tenant/page.tsx         # Displays injected tenant headers
│   ├── geofence/page.tsx       # Geofence checker UI
│   ├── realtime/page.tsx       # Socket.io counter UI
│   └── api/
│       ├── geofence/route.ts   # POST [lat, lng] → ST_Contains
│       └── tenant/route.ts     # GET — returns tenant headers as JSON
│
└── prisma/
    └── schema.prisma           # Minimal schema (geofence uses raw SQL only)

Environment variables

Variable Description Example
DATABASE_URL PostgreSQL connection string postgresql://user:pass@localhost:5432/geofence_demo
PORT Server port (default 3000) 3001
ROOT_HOST Root hostname for subdomain parsing (default localhost) lvh.me

Copy .env.example to .env and fill in DATABASE_URL.


Scripts

Script Description
npm run dev Start dev server (Next.js + Socket.io via server.ts)
npm run build Production build
npm start Run production build
npm run db:push Apply Prisma schema to the database
npm run db:generate Regenerate Prisma client

Production build

npm run build
npm start

Ensure DATABASE_URL points at a PostGIS-enabled PostgreSQL instance before starting.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages