- Custom SQLite task manager replacing TickTick wrapper - 73 tasks migrated from TickTick across 15 projects - RRULE recurrence engine with lazy materialization - Dashboard tasks widget (desktop sidebar + mobile card) - Tasks page with project tabs, add/edit/complete/delete - Security: locked ports to localhost, removed old containers - Gitea Actions runner configured and all 3 CI jobs passing - Fixed mobile overflow on dashboard cards - iOS Capacitor app shell (Second Brain) - Frontend/backend guide docs for adding new services - TickTick Google Calendar sync re-authorized Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7.3 KiB
7.3 KiB
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
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
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)
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)
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:
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:
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():
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
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:
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:
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/healthendpointservices/yourservice/Dockerfile— non-root user, healthcheck, minimal copydocker-compose.yml— service entry + gateway env vars + depends_ongateway/config.py— backend URL from envgateway/database.py— app registration in seedgateway/server.py— auth header injection in_proxy()gateway/proxy.py— add toNO_API_PREFIX_SERVICESif Nodefrontend-v2/src/routes/(app)/yourservice/+page.svelte— UI page- Nav registration in
+layout.server.ts,Navbar.svelte,MobileTabBar.svelte .env— addYOURSERVICE_API_KEY(or other secrets)docker compose build yourservice gateway && docker compose up -d