HiveMatrix Architecture - Core Concepts¶
Version 4.2 Part 1 of 3: Core Architecture Fundamentals
This document covers the fundamental architecture patterns, authentication flows, and service communication that form the foundation of HiveMatrix. Read this first before exploring development practices or service-specific implementations.
Other Parts: - Architecture - Development - Development practices, security, tools, and patterns - Architecture - Services - Service-specific deep dives (Brainhair, Ledger, KnowledgeTree)
1. Core Philosophy & Goals¶
This document is the single source of truth for the HiveMatrix architecture. Its primary audience is the AI development assistant responsible for writing and maintaining the platform's code. Adherence to these principles is mandatory.
Our goals are, in order of priority:
- AI Maintainability: Each individual application (e.g.,
Codex,Ledger) must remain small, focused, and simple. We sacrifice some traditional development conveniences to achieve this. - Modularity: The platform is a collection of independent, fully functional applications that can be composed together.
- Simplicity & Explicitness: We favor simple, explicit patterns over complex, "magical" ones. Assume code is correct and error out to expose flaws rather than building defensive checks.
2. The Monolithic Service Pattern¶
Each module in HiveMatrix (e.g., Codex, Ledger, KnowledgeTree) is a self-contained, monolithic application. Each application is a single, deployable unit responsible for its own business logic, database, and UI rendering.
- Server-Side Rendering: Applications must render their user interfaces on the server side, returning complete HTML documents.
- Data APIs: Applications may also expose data-only APIs (e.g.,
/api/tickets) that return JSON. - Data Isolation: Each service owns its own database. You are forbidden from accessing another service's database directly.
3. End-to-End Authentication Flow¶
The platform operates on a centralized login model orchestrated by Core and Nexus. No service handles user credentials directly. All authentication flows through Keycloak, and sessions are managed by Core with revocation support.
Initial Login Flow¶
- Initial Request: A user navigates to
https://your-server/(Nexus on port 443). - Auth Check:
Nexuschecks the user's session. If no valid session token exists, it stores the target URL and redirects to the login endpoint. - Keycloak Proxy: The user is redirected to
/keycloak/realms/hivematrix/protocol/openid-connect/auth. Nexus proxies this to the local Keycloak server (port 8080) with proper X-Forwarded headers. - Keycloak Login: User enters credentials on the Keycloak login page (proxied through Nexus).
- OAuth Callback: After successful login, Keycloak redirects to
https://your-server/keycloak-callbackwith an authorization code. - Token Exchange:
Nexusreceives the callback and:- Exchanges the authorization code for Keycloak access token (using backend localhost:8080 connection)
- Calls
Core's/api/token/exchangeendpoint with the Keycloak access token
- Session Creation:
Corereceives the Keycloak token and:- Validates it with Keycloak's userinfo endpoint
- Extracts user info and group membership
- Determines permission level from Keycloak groups
- Creates a server-side session with a unique session ID
- Mints a HiveMatrix JWT signed with Core's private RSA key containing:
- User identity (sub, name, email, preferred_username)
- Permission level (admin, technician, billing, or client)
- Group membership
- jti (JWT ID) - The session ID for revocation tracking
- Standard JWT claims (iss, iat, exp)
- 1-hour expiration (exp)
- Stores session in memory with TTL (Time To Live)
- JWT to Nexus:
Corereturns the HiveMatrix JWT toNexus. - Session Storage:
Nexusstores the JWT in the user's Flask session cookie. - Final Redirect:
Nexusredirects the user to their originally requested URL. - Authenticated Access: For subsequent requests:
Nexusretrieves the JWT from the session- Validates the JWT signature using Core's public key
- Checks with Core that the session (jti) hasn't been revoked
- If valid, proxies the request to backend services with
Authorization: Bearer <token>header
- Backend Verification: Backend services verify the JWT using Core's public key at
/.well-known/jwks.json.
Permission Levels¶
HiveMatrix supports four permission levels, determined by Keycloak group membership:
- admin: Members of the
adminsgroup - full system access - technician: Members of the
techniciansgroup - technical operations - billing: Members of the
billinggroup - financial operations - client: Default level for users not in any special group - limited access
Services can access the user's permission level via g.user.get('permission_level') and enforce authorization using the @admin_required decorator or custom permission checks.
Session Management & Logout Flow¶
HiveMatrix implements revokable sessions with automatic expiration to ensure proper security.
Session Lifecycle¶
Session Creation:
- When a user logs in, Core creates a server-side session with:
- Unique session ID (stored as jti in the JWT)
- User data (sub, name, email, permission_level, groups)
- Creation timestamp (created_at)
- Expiration timestamp (expires_at) - 1 hour from creation
- Revocation flag (revoked) - initially false
Session Validation:
- On each request, Nexus calls Core's /api/token/validate endpoint
- Core checks:
1. JWT signature is valid
2. JWT has not expired (exp claim)
3. Session ID (jti) exists in the session store
4. Session has not expired (expires_at)
5. Session has not been revoked (revoked flag)
- If any check fails, the session is invalid and the user must re-authenticate
Session Expiration: - Sessions automatically expire after 1 hour - Expired sessions are removed from memory during cleanup - Users must log in again after expiration
Logout Flow¶
- User Clicks Logout: User navigates to
/logoutendpoint on Nexus - Retrieve Token: Nexus retrieves the JWT from the user's session
- Revoke at Core: Nexus calls
Core's/api/token/revokewith the JWT: - Mark as Revoked: Core:
- Decodes the JWT to extract session ID (jti)
- Marks the session as revoked in the session store
- Returns success response
- Clear Client State: Nexus:
- Clears the server-side Flask session
- Returns HTML that clears browser storage and cookies
- Redirects to home page
- Re-authentication Required: Next request to any protected page:
- Nexus has no session → redirects to login
- OR if somehow a token is still cached → Core validation fails (session revoked)
Core Session Manager¶
The SessionManager class in hivematrix-core/app/session_manager.py provides:
class SessionManager:
def create_session(user_data) -> session_id
def validate_session(session_id) -> user_data or None
def revoke_session(session_id) -> bool
def cleanup_expired() -> count
Production Note: The current implementation uses in-memory storage. For production deployments with multiple Core instances, sessions should be stored in Redis or a database for shared state.
Core API Endpoints¶
Token Exchange:
POST /api/token/exchange
Body: { "access_token": "<keycloak_access_token>" }
Response: { "token": "<hivematrix_jwt>" }
Token Validation:
POST /api/token/validate
Body: { "token": "<hivematrix_jwt>" }
Response: { "valid": true, "user": {...} } or { "valid": false, "error": "..." }
Token Revocation:
POST /api/token/revoke
Body: { "token": "<hivematrix_jwt>" }
Response: { "message": "Session revoked successfully" }
Public Key (JWKS):
4. Service-to-Service Communication¶
Services may need to call each other's APIs (e.g., Treasury calling Codex to get billing data). This is done using service tokens minted by Core.
Service Token Flow¶
-
Request Service Token: The calling service (e.g., Treasury) makes a POST request to
Core's/service-tokenendpoint: -
Core Mints Token: Core creates a short-lived JWT (5 minutes) with:
-
Make Authenticated Request: The calling service uses this token in the Authorization header when calling the target service's API.
-
Target Service Verification: The target service verifies the token using Core's public key and checks the
typefield to determine if it's a service call.
Service Client Helper¶
All services include a service_client.py helper that automates this flow:
from app.service_client import call_service
# Make a service-to-service API call
response = call_service('codex', '/api/companies')
companies = response.json()
The call_service function:
- Automatically requests a service token from Core (with 5-minute caching - see Architecture - Services)
- Adds the Authorization header
- Makes the HTTP request
- Returns the response
Performance Note: Service tokens are cached for 4.5 minutes to reduce load on Core service. This caching reduces HTTP calls to Core by ~90% while maintaining security (tokens still expire after 5 minutes). See Service Token Caching for implementation details.
Service Discovery¶
Services are registered in two configuration files:
master_services.json - Master service registry (simplified format):
{
"codex": {
"url": "http://localhost:5010",
"port": 5010
},
"archive": {
"url": "http://localhost:5012",
"port": 5012
}
}
services.json - Full service configuration (extended format):
{
"codex": {
"url": "http://localhost:5010",
"path": "../hivematrix-codex",
"port": 5010,
"python_bin": "pyenv/bin/python",
"run_script": "run.py",
"visible": true
},
"ledger": {
"url": "http://localhost:5030",
"path": "../hivematrix-ledger",
"port": 5030,
"python_bin": "pyenv/bin/python",
"run_script": "run.py",
"visible": true
}
}
When adding a new service:
1. Add entry to master_services.json (required for Nexus service discovery)
2. Add extended entry to services.json (required for Helm service management)
3. Both files must be updated for the service to be properly discovered and started
Authentication Decorator Behavior¶
The @token_required decorator in each service handles both user and service tokens:
@token_required
def api_endpoint():
if g.is_service_call:
# Service-to-service call
calling_service = g.service
# Service calls bypass user-level permission checks
else:
# User call
user = g.user
# Apply user permission checks as needed
Service calls automatically bypass user-level permission requirements, as they represent trusted inter-service communication.
5. Frontend: The Smart Proxy Composition Model¶
The user interface is a composition of the independent applications, assembled by the Nexus proxy.
The Golden Rule of Styling¶
Applications are forbidden from containing their own styling. All visual presentation (CSS) is handled exclusively by Nexus injecting a global stylesheet. Applications must use the BEM classes defined in this document.
The Nexus Service¶
Nexus acts as the central gateway. Its responsibilities are:
* Enforcing authentication for all routes.
* Proxying requests to the appropriate backend service based on the URL path.
* Injecting the global global.css stylesheet into any HTML responses.
* Discovering backend services via the services.json file.
File: hivematrix-nexus/services.json
URL Prefix Handling with ProxyFix¶
When services are accessed through the Nexus proxy, they need to know their URL prefix to generate correct URLs. This is handled via X-Forwarded headers and werkzeug's ProxyFix middleware.
How Nexus Proxies Requests¶
- User requests:
https://192.168.1.233/knowledgetree/browse/ - Nexus strips the service prefix before forwarding
- Nexus adds X-Forwarded headers including
X-Forwarded-Prefix: /knowledgetree - Backend service receives:
/browse/with headers indicating the prefix - Backend's ProxyFix middleware sets SCRIPT_NAME from X-Forwarded-Prefix
- Flask's
url_for()generates correct URLs:/knowledgetree/browse/
Nexus Configuration¶
Nexus automatically adds X-Forwarded headers when proxying to backend services:
# In hivematrix-nexus/app/routes.py
headers['Authorization'] = f"Bearer {token}"
headers['X-Forwarded-For'] = request.remote_addr
headers['X-Forwarded-Proto'] = 'https' if request.is_secure else 'http'
headers['X-Forwarded-Host'] = request.host
headers['X-Forwarded-Prefix'] = f'/{service_name}' # e.g., /knowledgetree
Backend Service Configuration¶
Each service must use werkzeug's ProxyFix middleware to respect these headers:
# In app/__init__.py
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(
app.wsgi_app,
x_for=1, # Trust X-Forwarded-For
x_proto=1, # Trust X-Forwarded-Proto (http/https)
x_host=1, # Trust X-Forwarded-Host
x_prefix=1 # Trust X-Forwarded-Prefix (sets SCRIPT_NAME)
)
Important: Do NOT use custom PrefixMiddleware. Nexus already strips the prefix before forwarding, so the backend receives clean paths without the service name. The ProxyFix middleware only affects URL generation, not route matching.
Authentication for AJAX Requests¶
When making AJAX requests from frontend JavaScript, you must include credentials: 'same-origin' in fetch options:
This ensures the access_token cookie is sent with the request. The @token_required decorator checks both:
1. Authorization: Bearer <token> header (from Nexus proxy)
2. access_token cookie (from browser, as fallback)
# In app/auth.py - token_required decorator
auth_header = request.headers.get('Authorization')
token = None
if auth_header and auth_header.startswith('Bearer '):
token = auth_header.split(' ')[1]
else:
# Fall back to cookie
token = request.cookies.get('access_token')
6. Configuration Management & Auto-Installation¶
HiveMatrix uses a centralized configuration system managed by hivematrix-helm. All service configurations are generated and synchronized from Helm's master configuration.
Configuration Manager (config_manager.py)¶
The ConfigManager class in hivematrix-helm/config_manager.py is responsible for:
- Master Configuration Storage: Maintains
instance/configs/master_config.jsonwith system-wide settings - Per-App Configuration Generation: Generates
.flaskenvandinstance/[app].conffiles for each service - Centralized Settings: Ensures consistent Keycloak URLs, hostnames, and service URLs across all apps
Master Configuration Structure¶
{
"system": {
"hostname": "localhost",
"environment": "development",
"secret_key": "<generated>",
"log_level": "INFO"
},
"keycloak": {
"url": "http://localhost:8080",
"realm": "hivematrix",
"client_id": "core-client",
"client_secret": "<generated>",
"admin_username": "admin",
"admin_password": "admin"
},
"databases": {
"postgresql": {
"host": "localhost",
"port": 5432,
"admin_user": "postgres"
},
"neo4j": {
"uri": "bolt://localhost:7687",
"user": "neo4j",
"password": "password"
}
},
"apps": {
"codex": {
"port": 5010,
"database": "postgresql",
"db_name": "codex_db",
"db_user": "codex_user"
},
"ledger": {
"port": 5030,
"database": "postgresql",
"db_name": "ledger_db",
"db_user": "ledger_user"
}
}
}
.flaskenv Generation¶
The generate_app_dotenv(app_name) method creates .flaskenv files with:
- Flask Configuration:
FLASK_APP,FLASK_ENV,SECRET_KEY,SERVICE_NAME - Keycloak Configuration: Automatically adjusts URLs based on hostname (localhost vs production)
- For
core: Direct Keycloak connection (http://localhost:8080/realms/hivematrix) - For other services: Proxied URL (
https://hostname/keycloakorhttp://localhost:8080) - Service URLs:
CORE_SERVICE_URL,NEXUS_SERVICE_URL - Database Configuration:
DB_HOST,DB_PORT,DB_NAME(if database is configured) - JWT Configuration: For Core service only -
JWT_PRIVATE_KEY_FILE,JWT_PUBLIC_KEY_FILE, etc.
Example generated .flaskenv:
FLASK_APP=run.py
FLASK_ENV=development
SECRET_KEY=abc123...
SERVICE_NAME=codex
# Keycloak Configuration
KEYCLOAK_SERVER_URL=http://localhost:8080
KEYCLOAK_BACKEND_URL=http://localhost:8080
KEYCLOAK_REALM=hivematrix
KEYCLOAK_CLIENT_ID=core-client
# Service URLs
CORE_SERVICE_URL=http://localhost:5000
NEXUS_SERVICE_URL=http://localhost:8000
instance/app.conf Generation¶
The generate_app_conf(app_name) method creates ConfigParser-formatted files with:
- Database Section: PostgreSQL connection string with credentials
- App-Specific Sections: Custom configuration sections defined in master config
Example generated instance/codex.conf:
[database]
connection_string = postgresql://codex_user:password@localhost:5432/codex_db
db_host = localhost
db_port = 5432
db_name = codex_db
db_user = codex_user
Configuration Sync¶
To update all installed apps with current configuration:
This is automatically called by start.sh on each startup to ensure configurations are current.
Auto-Installation Architecture¶
HiveMatrix uses a registry-based installation system that allows services to be installed through the Helm web interface.
App Registry (apps_registry.json)¶
All installable apps are defined in hivematrix-helm/apps_registry.json. This file is the authoritative source for all HiveMatrix services and is used by install_manager.py to automatically generate both services.json and master_services.json.
{
"core_apps": {
"core": {
"name": "HiveMatrix Core",
"description": "Authentication & service registry - Required",
"git_url": "https://github.com/skelhammer/hivematrix-core",
"port": 5000,
"required": true,
"dependencies": ["postgresql"],
"install_order": 1
},
"nexus": {
"name": "HiveMatrix Nexus",
"description": "Frontend gateway and UI - Required",
"git_url": "https://github.com/skelhammer/hivematrix-nexus",
"port": 443,
"required": true,
"dependencies": ["core", "keycloak"],
"install_order": 2
}
},
"default_apps": {
"codex": {
"name": "HiveMatrix Codex",
"description": "Central data hub for MSP operations",
"git_url": "https://github.com/skelhammer/hivematrix-codex",
"port": 5010,
"required": false,
"dependencies": ["postgresql", "core"],
"install_order": 3
},
"ledger": {
"name": "HiveMatrix Ledger",
"description": "Billing calculations and invoicing (includes archive functionality)",
"git_url": "https://github.com/skelhammer/hivematrix-ledger",
"port": 5030,
"required": false,
"dependencies": ["postgresql", "core", "codex"],
"install_order": 4
}
},
"system_dependencies": {
"keycloak": {
"name": "Keycloak",
"description": "Authentication server",
"version": "26.4.0",
"download_url": "https://github.com/keycloak/keycloak/releases/download/26.4.0/keycloak-26.4.0.tar.gz",
"port": 8080,
"required": true,
"install_order": 0
},
"postgresql": {
"name": "PostgreSQL",
"description": "Relational database",
"apt_package": "postgresql postgresql-contrib",
"required": true
}
}
}
Installation Manager (install_manager.py)¶
The InstallManager class handles:
- Cloning Apps: Downloads from git repository
- Running Install Scripts: Executes
install.shif present - Dynamic Service Discovery: Automatically scans for ALL
hivematrix-*directories withrun.pyfiles - Service Registry Generation: Automatically generates both
master_services.jsonandservices.json - Checking Status: Monitors git status and available updates
Key Feature - Dynamic Service Discovery:
The scan_all_services() method automatically discovers all HiveMatrix services in the parent directory, not just those in apps_registry.json. This makes the system much more flexible:
- Registry Services: For services defined in
apps_registry.json, uses registry metadata (port, name, description) - Unknown Services: For services not in registry (e.g., manually copied or old versions), auto-generates configuration with smart port assignment
- No Manual Configuration: After
git pullor copying a service, it's automatically detected on next startup
How it works:
def scan_all_services(self):
"""Scan parent directory for all hivematrix-* services with run.py"""
discovered = {}
# Scan for all hivematrix-* directories
for item in self.parent_dir.iterdir():
if item.name.startswith('hivematrix-') and (item / 'run.py').exists():
service_name = item.name.replace('hivematrix-', '')
# Use registry info if available
app_info = self.registry.get(service_name)
if app_info:
discovered[service_name] = app_info
else:
# Auto-generate config for unknown services
discovered[service_name] = {
'name': f'HiveMatrix {service_name.title()}',
'port': 5000 + (hash(service_name) % 900),
'required': False
}
return discovered
Benefits:
- Copy old service versions → automatically detected
- Git pull new services → automatically registered
- No manual services.json edits required
- Works with any hivematrix-* service that has run.py
The update-config command reads both the registry AND scans the filesystem to generate service configuration files:
cd hivematrix-helm
source pyenv/bin/activate
# Install a new service
python install_manager.py install ledger
# Regenerate service configurations from apps_registry.json
python install_manager.py update-config
When to use update-config:
- After adding a new service to apps_registry.json
- After modifying service properties in apps_registry.json
- When service configuration files get out of sync
- This is automatically called by start.sh on startup
Or via Helm web interface.
Required Files for Auto-Installation¶
For a service to be installable via Helm, it must have:
1. install.sh - Installation script that:
- Creates Python virtual environment (python3 -m venv pyenv)
- Installs dependencies (pip install -r requirements.txt)
- Creates instance/ directory
- Creates initial .flaskenv (will be overwritten by config_manager)
- Symlinks services.json from Helm directory
- Runs any app-specific setup (database creation, etc.)
2. requirements.txt - Python dependencies:
Flask==3.0.0
python-dotenv==1.0.0
PyJWT==2.8.0
cryptography==41.0.7
SQLAlchemy==2.0.23
psycopg2-binary==2.9.9
3. run.py - Application entry point:
4. app/__init__.py - Flask app initialization (see Step 1 below)
5. services.json symlink - Created by install.sh, points to ../hivematrix-helm/services.json
Template install.sh Structure¶
#!/bin/bash
set -e # Exit on error
APP_NAME="myservice" # Replace with your service name
APP_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PARENT_DIR="$(dirname "$APP_DIR")"
HELM_DIR="$PARENT_DIR/hivematrix-helm"
# Create virtual environment
python3 -m venv pyenv
source pyenv/bin/activate
# Upgrade pip and install dependencies
pip install --upgrade pip
pip install -r requirements.txt
# Create instance directory
mkdir -p instance
# Create initial .flaskenv (will be regenerated by config_manager)
cat > .flaskenv <<EOF
FLASK_APP=run.py
FLASK_ENV=development
SERVICE_NAME=myservice
CORE_SERVICE_URL=http://localhost:5000
HELM_SERVICE_URL=http://localhost:5004
EOF
# Symlink services.json from Helm
if [ -d "$HELM_DIR" ] && [ -f "$HELM_DIR/services.json" ]; then
ln -sf "$HELM_DIR/services.json" services.json
fi
Updating Other Services¶
To make existing services installable via Helm:
- Add to
apps_registry.json: Define the service with git URL, port, and dependencies - Create
install.sh: Follow the template structure above - Test Installation: Run
python install_manager.py install <service> - Update Config: Ensure config_manager can generate proper .flaskenv and .conf files
Current Status: Template is the only fully working installable service. Codex has an install.sh but may need updates. Other services need install scripts created.
7. AI Instructions for Building a New Service¶
All new services (e.g., Codex, Architect) must be created by copying the hivematrix-template project. This ensures all necessary patterns are included.
Step 1: Configuration¶
Every service requires an app/__init__.py that loads its configuration from environment variables (via .flaskenv) and config files (via instance/[service].conf).
Important: The .flaskenv file is automatically generated by config_manager.py from Helm's master configuration. You should not manually edit .flaskenv files, as they will be overwritten on the next config sync.
File: [new-service]/app/__init__.py (Example)
from flask import Flask
import json
import os
app = Flask(__name__, instance_relative_config=True)
# --- Load all required configuration from environment variables ---
# These are set in .flaskenv, which is generated by config_manager.py
app.config['CORE_SERVICE_URL'] = os.environ.get('CORE_SERVICE_URL')
app.config['SERVICE_NAME'] = os.environ.get('SERVICE_NAME', 'myservice')
if not app.config['CORE_SERVICE_URL']:
raise ValueError("CORE_SERVICE_URL must be set in the .flaskenv file.")
# Load database connection from config file
# This file is generated by config_manager.py
import configparser
try:
os.makedirs(app.instance_path)
except OSError:
pass
config_path = os.path.join(app.instance_path, 'myservice.conf')
config = configparser.RawConfigParser() # Use RawConfigParser for special chars
config.read(config_path)
app.config['MYSERVICE_CONFIG'] = config
# Database configuration
app.config['SQLALCHEMY_DATABASE_URI'] = config.get('database', 'connection_string',
fallback=f"sqlite:///{os.path.join(app.instance_path, 'myservice.db')}")
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# Load services configuration for service-to-service calls
# This is symlinked from hivematrix-helm/services.json
try:
with open('services.json') as f:
services_config = json.load(f)
app.config['SERVICES'] = services_config
except FileNotFoundError:
print("WARNING: services.json not found. Service-to-service calls will not work.")
app.config['SERVICES'] = {}
from extensions import db
db.init_app(app)
# Apply ProxyFix to handle X-Forwarded headers from Nexus proxy
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(
app.wsgi_app,
x_for=1, # Trust X-Forwarded-For
x_proto=1, # Trust X-Forwarded-Proto
x_host=1, # Trust X-Forwarded-Host
x_prefix=1 # Trust X-Forwarded-Prefix (sets SCRIPT_NAME for url_for)
)
from app import routes
Configuration Files (Generated by Helm)
The following files are automatically generated by config_manager.py:
.flaskenv - Generated by config_manager.py generate_app_dotenv(app_name)
FLASK_APP=run.py
FLASK_ENV=development
SECRET_KEY=<auto-generated>
SERVICE_NAME=myservice
# Keycloak Configuration (auto-adjusted for environment)
KEYCLOAK_SERVER_URL=http://localhost:8080
KEYCLOAK_BACKEND_URL=http://localhost:8080
KEYCLOAK_REALM=hivematrix
KEYCLOAK_CLIENT_ID=core-client
# Service URLs
CORE_SERVICE_URL=http://localhost:5000
NEXUS_SERVICE_URL=http://localhost:8000
instance/myservice.conf - Generated by config_manager.py generate_app_conf(app_name)
[database]
connection_string = postgresql://myservice_user:password@localhost:5432/myservice_db
db_host = localhost
db_port = 5432
db_name = myservice_db
db_user = myservice_user
To regenerate these files after updating Helm's master config:
cd hivematrix-helm
source pyenv/bin/activate
python config_manager.py write-dotenv myservice
python config_manager.py write-conf myservice
# Or sync all apps at once:
python config_manager.py sync-all
Step 2: Securing Routes¶
All routes that display user data or perform actions must be protected by the @token_required decorator. This decorator handles JWT verification for both user and service tokens.
File: [new-service]/app/auth.py (Do not modify - copy from template)
from functools import wraps
from flask import request, g, current_app, abort
import jwt
jwks_client = None
def init_jwks_client():
"""Initializes the JWKS client from the URL in config."""
global jwks_client
core_url = current_app.config.get('CORE_SERVICE_URL')
if core_url:
jwks_client = jwt.PyJWKClient(f"{core_url}/.well-known/jwks.json")
def token_required(f):
"""
A decorator to protect routes, ensuring a valid JWT is present.
This now accepts both user tokens and service tokens.
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if jwks_client is None:
init_jwks_client()
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
abort(401, description="Authorization header is missing or invalid.")
token = auth_header.split(' ')[1]
try:
signing_key = jwks_client.get_signing_key_from_jwt(token)
data = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
issuer="hivematrix.core",
options={"verify_exp": True}
)
# Determine if this is a user token or service token
if data.get('type') == 'service':
# Service-to-service call
g.user = None
g.service = data.get('calling_service')
g.is_service_call = True
else:
# User call
g.user = data
g.service = None
g.is_service_call = False
except jwt.PyJWTError as e:
abort(401, description=f"Invalid Token: {e}")
return f(*args, **kwargs)
return decorated_function
def admin_required(f):
"""Decorator to require admin permission level."""
@wraps(f)
@token_required
def decorated_function(*args, **kwargs):
if g.is_service_call:
# Services can access admin routes
return f(*args, **kwargs)
if not g.user or g.user.get('permission_level') != 'admin':
abort(403, description="Admin access required.")
return f(*args, **kwargs)
return decorated_function
File: [new-service]/app/routes.py (Example)
from flask import render_template, g, jsonify
from app import app
from .auth import token_required, admin_required
@app.route('/')
@token_required
def index():
# Prevent service calls from accessing UI routes
if g.is_service_call:
return {'error': 'This endpoint is for users only'}, 403
# The user's information is available in the 'g.user' object
user = g.user
return render_template('index.html', user=user)
@app.route('/api/data')
@token_required
def api_data():
# This endpoint works for both users and services
if g.is_service_call:
# Service-to-service call from g.service
return jsonify({'data': 'service response'})
else:
# User call - can check permissions
if g.user.get('permission_level') != 'admin':
return {'error': 'Admin only'}, 403
return jsonify({'data': 'user response'})
@app.route('/admin/settings')
@admin_required
def admin_settings():
# Only admins can access this
return render_template('admin/settings.html', user=g.user)
Step 3: Building the UI Template¶
HTML templates must be unstyled and use the BEM classes from the design system. User data from the JWT is passed into the template.
File: [new-service]/app/templates/index.html (Example)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My New Service</title>
</head>
<body>
<div class="card">
<div class="card__header">
<h1 class="card__title">Hello, {{ user.name }}!</h1>
</div>
<div class="card__body">
<p>Your username is: <strong>{{ user.preferred_username }}</strong></p>
<p>Permission level: <strong>{{ user.permission_level }}</strong></p>
<button class="btn btn--primary">
<span class="btn__label">Primary Action</span>
</button>
</div>
</div>
</body>
</html>
Step 4: Database Initialization¶
Create an init_db.py script to interactively set up the database:
import os
import sys
import configparser
from getpass import getpass
from sqlalchemy import create_engine
from dotenv import load_dotenv
load_dotenv('.flaskenv')
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from app import app
from extensions import db
from models import YourModel1, YourModel2 # Import your models
def get_db_credentials(config):
"""Prompts the user for PostgreSQL connection details."""
print("\n--- PostgreSQL Database Configuration ---")
# Load existing or use defaults
db_details = {
'host': config.get('database_credentials', 'db_host', fallback='localhost'),
'port': config.get('database_credentials', 'db_port', fallback='5432'),
'user': config.get('database_credentials', 'db_user', fallback='myservice_user'),
'dbname': config.get('database_credentials', 'db_dbname', fallback='myservice_db')
}
host = input(f"Host [{db_details['host']}]: ") or db_details['host']
port = input(f"Port [{db_details['port']}]: ") or db_details['port']
dbname = input(f"Database Name [{db_details['dbname']}]: ") or db_details['dbname']
user = input(f"User [{db_details['user']}]: ") or db_details['user']
password = getpass("Password: ")
return {'host': host, 'port': port, 'dbname': dbname, 'user': user, 'password': password}
def test_db_connection(creds):
"""Tests the database connection."""
from urllib.parse import quote_plus
escaped_password = quote_plus(creds['password'])
conn_string = f"postgresql://{creds['user']}:{escaped_password}@{creds['host']}:{creds['port']}/{creds['dbname']}"
try:
engine = create_engine(conn_string)
with engine.connect() as connection:
print("\n✓ Database connection successful!")
return conn_string, True
except Exception as e:
print(f"\n✗ Connection failed: {e}", file=sys.stderr)
return None, False
def init_db():
"""Interactively configures and initializes the database."""
instance_path = app.instance_path
config_path = os.path.join(instance_path, 'myservice.conf')
config = configparser.RawConfigParser()
if os.path.exists(config_path):
config.read(config_path)
print(f"\n✓ Existing configuration found: {config_path}")
else:
print(f"\n→ Creating new config: {config_path}")
os.makedirs(instance_path, exist_ok=True)
# Database configuration
while True:
creds = get_db_credentials(config)
conn_string, success = test_db_connection(creds)
if success:
if not config.has_section('database'):
config.add_section('database')
config.set('database', 'connection_string', conn_string)
if not config.has_section('database_credentials'):
config.add_section('database_credentials')
for key, val in creds.items():
if key != 'password':
config.set('database_credentials', f'db_{key}', val)
break
else:
if input("\nRetry? (y/n): ").lower() != 'y':
sys.exit("Database configuration aborted.")
# Save configuration
with open(config_path, 'w') as configfile:
config.write(configfile)
print(f"\n✓ Configuration saved to: {config_path}")
# Initialize database schema
with app.app_context():
print("\nInitializing database schema...")
db.create_all()
print("✓ Database schema initialized successfully!")
if __name__ == '__main__':
init_db()
Continue to: - Architecture - Development - Running environment, debugging, security, design system - Architecture - Services - Brainhair, Ledger, KnowledgeTree, production features