# Adding a New Backend Service Step-by-step for adding a new backend service to the platform. --- ## 1. Create the service directory ``` services/yourservice/ server.py # or server.js Dockerfile .env # (optional, for local secrets) data/ # (created at runtime, for SQLite services) ``` ## 2. Python service template ```python import json import os import sqlite3 from http.server import HTTPServer, BaseHTTPRequestHandler from pathlib import Path from threading import Lock PORT = int(os.environ.get("PORT", 8099)) DATA_DIR = Path(os.environ.get("DATA_DIR", "/app/data")) DB_PATH = DATA_DIR / "yourservice.db" # --- Database --- def get_db(): conn = sqlite3.connect(str(DB_PATH)) conn.row_factory = sqlite3.Row conn.execute("PRAGMA foreign_keys = ON") conn.execute("PRAGMA journal_mode = WAL") return conn def init_db(): DATA_DIR.mkdir(parents=True, exist_ok=True) conn = get_db() c = conn.cursor() c.execute('''CREATE TABLE IF NOT EXISTS items ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )''') conn.commit() conn.close() # --- Migrations (add columns safely) --- # try: # c.execute("ALTER TABLE items ADD COLUMN description TEXT DEFAULT ''") # except: pass # --- Handler --- class Handler(BaseHTTPRequestHandler): def _read_body(self): length = int(self.headers.get("Content-Length", 0)) return json.loads(self.rfile.read(length)) if length else {} def _send_json(self, data, status=200): body = json.dumps(data).encode() self.send_response(status) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def do_GET(self): path = self.path.split("?")[0] # Health check (before auth, unauthenticated) if path == "/api/health": self._send_json({"status": "ok"}) return # --- Your routes --- if path == "/api/items": conn = get_db() rows = conn.execute("SELECT * FROM items ORDER BY created_at DESC").fetchall() conn.close() self._send_json({"items": [dict(r) for r in rows]}) return self._send_json({"error": "Not found"}, 404) def do_POST(self): path = self.path.split("?")[0] body = self._read_body() if path == "/api/items": conn = get_db() c = conn.cursor() c.execute("INSERT INTO items (name) VALUES (?)", (body.get("name", ""),)) conn.commit() item_id = c.lastrowid conn.close() self._send_json({"id": item_id}, 201) return self._send_json({"error": "Not found"}, 404) def log_message(self, format, *args): pass # Suppress request logs (gateway logs instead) # --- Start --- if __name__ == "__main__": init_db() from http.server import ThreadingHTTPServer server = ThreadingHTTPServer(("0.0.0.0", PORT), Handler) print(f"Service listening on port {PORT}") server.serve_forever() ``` ## 3. Node/Express service template ```javascript const express = require('express'); const app = express(); const port = process.env.PORT || 3099; app.use(express.json()); // Health (before auth middleware) app.get('/health', (req, res) => res.json({ status: 'ok' })); // API key auth middleware const SERVICE_API_KEY = process.env.SERVICE_API_KEY || ''; if (SERVICE_API_KEY) { app.use((req, res, next) => { const key = req.headers['x-api-key'] || req.query.api_key; if (key !== SERVICE_API_KEY) { return res.status(401).json({ error: 'Unauthorized' }); } next(); }); } // Routes app.get('/items', async (req, res) => { // ... }); app.listen(port, () => console.log(`Listening on ${port}`)); ``` ## 4. Dockerfile (Python) ```dockerfile FROM python:3.12-slim WORKDIR /app # Install dependencies (add as needed) RUN pip install --no-cache-dir bcrypt # Non-root user RUN adduser --disabled-password --no-create-home appuser RUN mkdir -p /app/data && chown -R appuser /app/data COPY --chown=appuser server.py . EXPOSE 8099 ENV PYTHONUNBUFFERED=1 HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ CMD python3 -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8099/api/health', timeout=3)" || exit 1 USER appuser CMD ["python3", "server.py"] ``` ## 5. Dockerfile (Node) ```dockerfile FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm install --production COPY server.js ./ EXPOSE 3099 ENV NODE_ENV=production HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ CMD wget -qO- http://127.0.0.1:3099/health || exit 1 USER node CMD ["node", "server.js"] ``` ## 6. Docker Compose entry Add to `docker-compose.yml`: ```yaml yourservice: build: context: ./services/yourservice dockerfile: Dockerfile container_name: platform-yourservice restart: unless-stopped volumes: - ./services/yourservice/data:/app/data environment: - PORT=8099 - SERVICE_API_KEY=${YOURSERVICE_API_KEY} # for API-key auth - TZ=${TZ:-America/Chicago} ``` Add to the `gateway` service: ```yaml gateway: environment: - YOURSERVICE_BACKEND_URL=http://yourservice:8099 - YOURSERVICE_API_KEY=${YOURSERVICE_API_KEY} depends_on: - yourservice ``` ## 7. Gateway integration ### a) Register the app in `gateway/database.py` Add a migration block in `_seed_apps()`: ```python try: c.execute("""INSERT INTO apps (id, name, icon, route_prefix, proxy_target, sort_order, enabled, dashboard_widget) VALUES ('yourservice', 'Your Service', 'icon-name', '/yourservice', ?, 8, 1, NULL)""", (YOURSERVICE_BACKEND_URL,)) except: pass ``` ### b) Add URL to `gateway/config.py` ```python YOURSERVICE_BACKEND_URL = os.environ.get("YOURSERVICE_BACKEND_URL", "http://yourservice:8099") ``` ### c) Add auth injection in `gateway/server.py` In the `_proxy()` method, add to the credential injection block: ```python elif service_id == "yourservice": headers["X-API-Key"] = YOURSERVICE_API_KEY ``` ### d) If Node/Express: skip `/api` prefix In `gateway/proxy.py`, add to `NO_API_PREFIX_SERVICES`: ```python NO_API_PREFIX_SERVICES = {"inventory", "budget", "yourservice"} ``` This makes `/api/yourservice/items` proxy to `http://yourservice:8099/items` instead of `http://yourservice:8099/api/items`. Python services typically expect the `/api` prefix, so don't add them here. ## 8. Checklist - [ ] `services/yourservice/server.py` (or `.js`) with `/health` endpoint - [ ] `services/yourservice/Dockerfile` — non-root user, healthcheck, minimal copy - [ ] `docker-compose.yml` — service entry + gateway env vars + depends_on - [ ] `gateway/config.py` — backend URL from env - [ ] `gateway/database.py` — app registration in seed - [ ] `gateway/server.py` — auth header injection in `_proxy()` - [ ] `gateway/proxy.py` — add to `NO_API_PREFIX_SERVICES` if Node - [ ] `frontend-v2/src/routes/(app)/yourservice/+page.svelte` — UI page - [ ] Nav registration in `+layout.server.ts`, `Navbar.svelte`, `MobileTabBar.svelte` - [ ] `.env` — add `YOURSERVICE_API_KEY` (or other secrets) - [ ] `docker compose build yourservice gateway && docker compose up -d`