Files
boc/aamos-admin-upgrade
Bernt 6de2455917 v1.2.0: Add Global Markets footer, translated to 9 languages
- Added GLOBAL_MARKETS_TITLE to all translation files
- Updated footer with 12 markets (4 active + 8 upcoming)
- Translated market section to: zh-cn, zh-tw, ja, ko, th, vi, id, ms, hi
- Built and deployed to production
- CloudFront invalidation: I3RTMXVFDJXWLG3SYX208OP1CC
2026-07-08 19:56:03 +00:00
..

AAMOS Admin

Administration console for the AAMOS platform — system dashboard, module control, user management, and real-time metrics.

Version: 1.0.0
Stack: Go 1.25 · Rust 2021 · SwiftUI (iOS) · Jetpack Compose (Android) · Vanilla HTML/JS (Web)


Table of Contents

  1. Architecture
  2. Prerequisites
  3. Environment Variables
  4. Go Backend — Setup & Run
  5. Rust Core — Build
  6. Web — Deploy
  7. iOS — Xcode Setup
  8. Android — Android Studio Setup
  9. API Reference
  10. WebSocket
  11. Database Schema
  12. Deployment Guide

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                         AAMOS Admin                             │
│                                                                 │
│   Clients                      Backend                          │
│  ─────────                    ─────────                         │
│                                                                 │
│  ┌──────────┐                 ┌─────────────────────────────┐   │
│  │  Web UI  │─── HTTP/WS ──▶ │     Go HTTP Server :8080    │   │
│  │ (static) │                │                             │   │
│  └──────────┘                │  /health        HealthH.    │   │
│                              │  /auth/*        AuthH.      │   │
│  ┌──────────┐                │  /api/v1/       ─────────── │   │
│  │  iOS App │─── HTTPS ───▶  │    dashboard    DashboardH. │   │
│  │ SwiftUI  │                │    metrics      MetricsH.   │   │
│  └──────────┘                │    modules      ModulesH.   │   │
│                              │    audit        AuditH.     │   │
│  ┌──────────┐                │    settings     SettingsH.  │   │
│  │ Android  │─── HTTPS ───▶  │    users        UsersH.     │   │
│  │ Compose  │                │    onboarding   OnboardH.   │   │
│  └──────────┘                │  /ws            WebSocketH. │   │
│                              └─────────────┬───────────────┘   │
│                                            │                    │
│                              ┌─────────────▼───────────────┐   │
│                              │      PostgreSQL              │   │
│                              │  users · modules · audit     │   │
│                              │  settings · onboarding       │   │
│                              └─────────────────────────────┘   │
│                                                                 │
│   Security Layer                                                │
│  ──────────────                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │              Rust Core  (aamos-core.so)                 │   │
│  │                                                         │   │
│  │   crypto.rs      argon2 + random bytes                  │   │
│  │   jwt.rs         token sign / verify (HS256)            │   │
│  │   ratelimit.rs   sliding-window rate limiting           │   │
│  │   sanitize.rs    input sanitisation / escaping          │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│   Middleware chain (every request)                              │
│   Request ──▶ CORS ──▶ Logging ──▶ JWTAuth* ──▶ Handler        │
│               (* public routes bypass JWTAuth)                  │
└─────────────────────────────────────────────────────────────────┘

Directory Layout

aamos-admin/
├── backend/              Go HTTP API
│   ├── config/           Environment config loader
│   ├── db/               PostgreSQL connection + auto-migration
│   ├── handlers/         HTTP handler implementations
│   ├── middleware/        CORS, logging, JWT auth
│   ├── models/           Shared Go types
│   ├── go.mod
│   └── go.sum
├── core/                 Rust security library
│   ├── src/
│   │   ├── lib.rs
│   │   ├── crypto.rs
│   │   ├── jwt.rs
│   │   ├── ratelimit.rs
│   │   └── sanitize.rs
│   └── Cargo.toml
├── web/                  Static admin console
│   ├── index.html        Main dashboard
│   ├── modules.html      Module management
│   └── assets/
├── ios/                  iOS native app (SwiftUI)
│   └── AamosAdmin/
│       ├── AamosAdmin.xcodeproj
│       ├── Services/     AuthService (Keychain)
│       ├── ViewModels/   Auth, Dashboard
│       ├── Views/        Login, Modules, Audit, Settings
│       ├── Components/   HealthRing, MetricCard, ModuleToggleRow
│       └── Models/
├── android/              Android native app (Compose)
│   ├── app/
│   │   └── src/main/java/com/aamos/admin/
│   ├── app/build.gradle.kts
│   └── settings.gradle.kts
└── shared/               Shared TypeScript types & constants

Prerequisites

Tool Minimum Version Install
Go 1.25.0 brew install go / go.dev/dl
Rust + Cargo 1.78 curl https://sh.rustup.rs -sSf | sh
PostgreSQL 14 brew install postgresql@14
Xcode 16.0 Mac App Store
Android Studio Meerkat (2025.1) developer.android.com
Node.js 20+ (web serve only) brew install node

Environment Variables

All variables are read at startup. Missing variables fall back to the listed defaults.

Variable Default Required in prod Description
PORT 8080 HTTP listen port
DB_URL postgres://postgres:postgres@localhost:5432/aamos?sslmode=disable YES PostgreSQL connection string
JWT_SECRET change-me-in-production YES HMAC-SHA256 signing secret (min 32 chars)
AMOS_BASE_URL http://localhost:9000 Base URL for upstream AMOS service calls
CORS_ORIGINS http://localhost:3000 Comma-separated allowed CORS origins

Create a local .env file (not committed):

PORT=8080
DB_URL=postgres://postgres:secret@localhost:5432/aamos?sslmode=disable
JWT_SECRET=replace-with-64-char-random-string
AMOS_BASE_URL=http://localhost:9000
CORS_ORIGINS=http://localhost:3000,http://localhost:8080

Go Backend

1. Install dependencies

cd backend
go mod download

2. Start PostgreSQL

# macOS (Homebrew)
brew services start postgresql@14

# Linux
sudo systemctl start postgresql

# Create database
createdb aamos

The backend runs auto-migration on startup — no manual schema steps required.

3. Run in development

cd backend

# Export env vars, then:
go run ./cmd/main.go

Or inline:

DB_URL="postgres://postgres:secret@localhost:5432/aamos?sslmode=disable" \
JWT_SECRET="dev-secret-change-me" \
go run ./cmd/main.go

Server starts on http://localhost:8080.

4. Build binary

cd backend
CGO_ENABLED=0 GOOS=linux go build -o aamos-admin ./cmd/main.go
./aamos-admin

5. Run tests

cd backend
go test ./...

Rust Core

The aamos-core crate compiles as both a dynamic library (cdylib) and a Rust library (rlib). The Go backend links against the compiled .so at runtime for cryptographic operations.

Build

cd core

# Development
cargo build

# Production (optimised — LTO, abort on panic)
cargo build --release

Output artifacts:

File Platform
target/release/libaamos_core.so Linux
target/release/libaamos_core.dylib macOS
target/release/libaamos_core.a Static archive

Run tests

cd core
cargo test

Modules

Module Purpose
crypto Argon2id password hashing, random byte generation
jwt HS256 token sign and verify
ratelimit Sliding-window rate limiting (no external state)
sanitize Input sanitisation: HTML escape, trim, length enforcement

Web Deploy

The web frontend is a static site — no build step required.

Local development

cd web
python3 -m http.server 3000
# or
npx serve .

Open http://localhost:3000.

Production (nginx)

server {
    listen 80;
    server_name admin.example.com;

    root /opt/aamos-admin/web;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location /api/ {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /auth/ {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /ws {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

iOS — Xcode Setup

Requirements

  • Mac with Apple silicon or Intel
  • Xcode 16.0 or later
  • iOS 18.0+ device or simulator

Step-by-step

1. Open the project

File > Open...

Navigate to aamos-admin/ios/ and select AamosAdmin.xcodeproj.

2. Set the development team

  • In the Project navigator, select AamosAdmin (root)
  • Select the AamosAdmin target
  • Open the Signing & Capabilities tab
  • Under Signing, set Team to your Apple Developer account

3. Configure the API base URL

The base URL is set in AuthViewModel.swift:

AamosAdmin/ViewModels/AuthViewModel.swift

Change the baseURL constant:

private let baseURL = "https://admin-api.example.com"

For local development against the Go backend:

private let baseURL = "http://localhost:8080"

iOS blocks HTTP by default. For local HTTP targets, add an App Transport Security exception in Info.plist:

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

4. Select a simulator or device

  • Click the device selector in the Xcode toolbar
  • Choose iPhone 16 Pro (or any iOS 18+ simulator)
  • Or connect a physical device

5. Build and run

Product > Run   (⌘R)

6. Authentication notes

The app stores JWT tokens in the iOS Keychain with kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly. The token persists across restarts. To clear it during development, delete and reinstall the app.

Project reference

Setting Value
Bundle ID com.aamos.admin
Keychain service com.aamos.admin
Min deployment target iOS 18.0
UI framework SwiftUI

App structure

AamosAdmin/
├── Services/
│   └── AuthService.swift       Keychain JWT storage (read/write/delete)
├── ViewModels/
│   ├── AuthViewModel.swift     Login / logout state machine
│   └── DashboardViewModel.swift Live metric polling
├── Views/
│   ├── LoginView.swift
│   ├── DashboardView.swift
│   ├── ModulesView.swift
│   ├── AuditView.swift
│   └── SettingsView.swift
└── Components/
    ├── HealthRing.swift         System health ring visualisation
    ├── MetricCard.swift         CPU / RAM / disk display card
    └── ModuleToggleRow.swift    Module enable / disable row

Android — Android Studio Setup

Requirements

  • Android Studio Meerkat (2025.1) or later
  • JDK 17 (bundled with Android Studio)
  • Android SDK API 35 (install via SDK Manager)
  • Device or emulator running API 26+ (Android 8.0)

Step-by-step

1. Open the project

File > Open...

Select the aamos-admin/android/ folder. Android Studio detects settings.gradle.kts and imports automatically. Wait for the initial Gradle sync (first run downloads ~500 MB).

2. Gradle sync

If sync does not start:

File > Sync Project with Gradle Files

3. Configure the API base URL

Locate the Retrofit base URL constant in the DI network module (e.g. di/NetworkModule.kt):

private const val BASE_URL = "https://admin-api.example.com/"

For the Android Emulator (routes 10.0.2.2 to host localhost):

private const val BASE_URL = "http://10.0.2.2:8080/"

For a physical device on the same LAN as the dev machine:

private const val BASE_URL = "http://192.168.1.x:8080/"

4. Create or start an emulator

Tools > Device Manager > Create Device

Choose Pixel 9 ProAPI 35 system image → Finish.

5. Build and run

Run > Run 'app'   (Shift+F10)

6. Build release APK / AAB

Build > Generate Signed App Bundle / APK...

Release builds enable ProGuard (isMinifyEnabled = true) and resource shrinking.

Project configuration

Setting Value
Application ID com.aamos.admin
Min SDK API 26 (Android 8.0)
Target SDK API 35 (Android 15)
Kotlin 2.1.0
Compose BOM 2025.04.01
Hilt 2.56.1
Retrofit 2.11.0
OkHttp 4.12.0
Navigation Compose 2.8.9
Lifecycle / ViewModel 2.9.0

API Reference

Base URL (development): http://localhost:8080

All endpoints that require authentication expect:

Authorization: Bearer <jwt-token>

JWT tokens expire after 24 hours.


Health

GET /health

No authentication required.

curl http://localhost:8080/health
{
  "ok": true,
  "version": "1.0.0",
  "uptime": "4h32m15s"
}

Authentication

POST /auth/login

curl -X POST http://localhost:8080/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "admin@example.com", "password": "secret"}'

200 OK

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user": {
    "id": "01234567-89ab-cdef-0123-456789abcdef",
    "email": "admin@example.com",
    "name": "Admin User",
    "role": "admin"
  }
}

401 Unauthorized — invalid credentials

{ "error": "invalid credentials" }

POST /auth/logout

JWT is stateless — the client discards the token. Server confirms.

curl -X POST http://localhost:8080/auth/logout \
  -H "Authorization: Bearer $TOKEN"
{ "message": "logged out" }

GET /auth/me

curl http://localhost:8080/auth/me \
  -H "Authorization: Bearer $TOKEN"
{
  "id": "01234567-89ab-cdef-0123-456789abcdef",
  "email": "admin@example.com",
  "name": "Admin User",
  "role": "admin"
}

Dashboard

GET /api/v1/dashboard

Aggregated view: system metrics, module states, and recent audit summary.

curl http://localhost:8080/api/v1/dashboard \
  -H "Authorization: Bearer $TOKEN"

System Metrics

GET /api/v1/metrics

Live CPU, RAM, disk, and process counts. CPU is sampled twice with a 200 ms interval.

curl http://localhost:8080/api/v1/metrics \
  -H "Authorization: Bearer $TOKEN"
{
  "timestamp": "2026-05-10T14:23:00Z",
  "cpu": { "usage_percent": 12.50 },
  "ram": {
    "total_bytes": 8589934592,
    "used_bytes": 4123456789,
    "free_bytes": 1073741824,
    "available_bytes": 2147483648,
    "usage_percent": 48.01
  },
  "disk": {
    "path": "/",
    "total_bytes": 107374182400,
    "used_bytes": 53687091200,
    "free_bytes": 53687091200,
    "usage_percent": 50.00
  },
  "processes": { "total": 312, "running": 4 }
}

Modules

The 10 Ouroboros modules are seeded automatically on first start.

GET /api/v1/modules

curl http://localhost:8080/api/v1/modules \
  -H "Authorization: Bearer $TOKEN"
[
  {
    "id": "academy",
    "name": "Academy",
    "description": "Learning & Training Platform",
    "icon": "graduation-cap",
    "enabled": false,
    "status": "stopped"
  },
  {
    "id": "finance",
    "name": "Finance",
    "description": "Financial Management & Reporting",
    "icon": "chart-line",
    "enabled": true,
    "status": "running"
  }
]

Module catalogue:

ID Name Description
academy Academy Learning & Training Platform
finance Finance Financial Management & Reporting
compliance Compliance Regulatory Compliance & Auditing
people People HR & People Management
operations Operations Operations & Process Management
commerce Commerce Sales & Commerce Platform
identity Identity Identity & Access Management
connect Connect Communications & Integration Hub
governance Governance Corporate Governance & Policy
analytics Analytics Data Analytics & Business Intelligence

PUT /api/v1/modules/{id}/toggle

Flips enabled and syncs status (runningstopped).

curl -X PUT http://localhost:8080/api/v1/modules/finance/toggle \
  -H "Authorization: Bearer $TOKEN"
{
  "id": "finance",
  "name": "Finance",
  "description": "Financial Management & Reporting",
  "icon": "chart-line",
  "enabled": true,
  "status": "running"
}

Audit Log

GET /api/v1/audit

Paginated, newest first.

Query parameters:

Parameter Type Default Description
page int 1 Page number
limit int 20 Page size (max 100)
user string Filter by user_id
action string Filter by action name
resource string Filter by resource name
curl "http://localhost:8080/api/v1/audit?page=1&limit=5&action=login" \
  -H "Authorization: Bearer $TOKEN"
{
  "data": [
    {
      "id": "a1b2c3d4-...",
      "user_id": "01234567-...",
      "action": "login",
      "resource": "auth",
      "ip": "127.0.0.1",
      "timestamp": "2026-05-10T14:00:00Z",
      "details": ""
    }
  ],
  "total": 42,
  "page": 1,
  "limit": 5
}

Settings

GET /api/v1/settings

curl http://localhost:8080/api/v1/settings \
  -H "Authorization: Bearer $TOKEN"
{
  "maintenance_mode": "false",
  "max_login_attempts": "5"
}

PUT /api/v1/settings

Upserts one or more key-value pairs atomically.

curl -X PUT http://localhost:8080/api/v1/settings \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"maintenance_mode": "true", "max_login_attempts": "3"}'

Returns the updated keys:

{
  "maintenance_mode": "true",
  "max_login_attempts": "3"
}

Users

GET /api/v1/users?page=1&limit=20

curl "http://localhost:8080/api/v1/users?page=1&limit=20" \
  -H "Authorization: Bearer $TOKEN"
{
  "users": [
    {
      "id": "01234567-89ab-cdef-0123-456789abcdef",
      "email": "admin@example.com",
      "name": "Admin User",
      "role": "admin",
      "created_at": "2026-01-01T00:00:00Z",
      "last_login": "2026-05-10T14:00:00Z"
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 20
}

POST /api/v1/users

Role must be admin or user (defaults to user).

curl -X POST http://localhost:8080/api/v1/users \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "alice@example.com",
    "name": "Alice",
    "role": "user",
    "password": "strong-password-here"
  }'

201 Created

{
  "id": "abcdef01-...",
  "email": "alice@example.com",
  "name": "Alice",
  "role": "user",
  "created_at": "2026-05-10T14:30:00Z",
  "last_login": "2026-05-10T14:30:00Z"
}

409 Conflict — email already exists

GET /api/v1/users/{id}

curl http://localhost:8080/api/v1/users/abcdef01-... \
  -H "Authorization: Bearer $TOKEN"

PUT /api/v1/users/{id}

Partial update. Omit role to preserve the existing value.

curl -X PUT http://localhost:8080/api/v1/users/abcdef01-... \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice Smith", "role": "admin"}'

DELETE /api/v1/users/{id}

Soft delete — sets deleted_at. The user disappears from all list and lookup queries.

curl -X DELETE http://localhost:8080/api/v1/users/abcdef01-... \
  -H "Authorization: Bearer $TOKEN"

204 No Content


Onboarding

POST /api/v1/onboarding/start

Creates an onboarding session.

curl -X POST http://localhost:8080/api/v1/onboarding/start \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"steps": ["create_admin", "enable_modules", "configure_settings"]}'

201 Created

{
  "id": "d4e5f6a7b8c9...",
  "steps": ["create_admin", "enable_modules", "configure_settings"],
  "started_at": "2026-05-10T14:00:00Z",
  "status": "pending"
}

WebSocket

Real-time system metrics are pushed to every connected client every 5 seconds.

Endpoint: ws://localhost:8080/ws

Frame format — same shape as GET /api/v1/metrics:

{
  "timestamp": "2026-05-10T14:23:05Z",
  "cpu": { "usage_percent": 8.25 },
  "ram": {
    "total_bytes": 8589934592,
    "used_bytes": 4000000000,
    "free_bytes": 1000000000,
    "available_bytes": 2000000000,
    "usage_percent": 46.57
  },
  "disk": {
    "path": "/",
    "total_bytes": 107374182400,
    "used_bytes": 50000000000,
    "free_bytes": 50000000000,
    "usage_percent": 46.57
  },
  "processes": { "total": 310, "running": 3 }
}

Browser example:

const ws = new WebSocket('ws://localhost:8080/ws');
ws.onmessage = (event) => {
  const m = JSON.parse(event.data);
  console.log('CPU:', m.cpu.usage_percent, '%');
};

Keepalive: Server sends a WebSocket PING every 54 seconds. Clients that do not respond within 60 seconds are disconnected.


Database Schema

Auto-migrated on every backend startup (idempotent — safe to restart).

CREATE TABLE IF NOT EXISTS users (
    id            TEXT        PRIMARY KEY,
    email         TEXT        NOT NULL UNIQUE,
    name          TEXT        NOT NULL DEFAULT '',
    role          TEXT        NOT NULL DEFAULT 'user',   -- 'admin' | 'user'
    password_hash TEXT        NOT NULL DEFAULT '',
    created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    last_login    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    deleted_at    TIMESTAMPTZ                            -- soft delete
);

CREATE TABLE IF NOT EXISTS modules (
    id          TEXT    PRIMARY KEY,
    name        TEXT    NOT NULL UNIQUE,
    enabled     BOOLEAN NOT NULL DEFAULT FALSE,
    description TEXT    NOT NULL DEFAULT '',
    icon        TEXT    NOT NULL DEFAULT '',
    status      TEXT    NOT NULL DEFAULT 'stopped'       -- 'running' | 'stopped'
);

CREATE TABLE IF NOT EXISTS audit_logs (
    id        TEXT        PRIMARY KEY,
    user_id   TEXT        NOT NULL REFERENCES users(id) ON DELETE SET NULL,
    action    TEXT        NOT NULL,
    resource  TEXT        NOT NULL DEFAULT '',
    ip        TEXT        NOT NULL DEFAULT '',
    timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    details   TEXT        NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_audit_logs_user_id   ON audit_logs(user_id);
CREATE INDEX IF NOT EXISTS idx_audit_logs_timestamp ON audit_logs(timestamp DESC);

CREATE TABLE IF NOT EXISTS settings (
    key        TEXT        PRIMARY KEY,
    value      TEXT        NOT NULL DEFAULT '',
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS onboarding_sessions (
    id         TEXT        PRIMARY KEY,
    steps      TEXT        NOT NULL,   -- JSON array
    started_at TIMESTAMPTZ NOT NULL,
    status     TEXT        NOT NULL DEFAULT 'pending'
);

Connection pool (configured in db/postgres.go):

Setting Value
Max open connections 25
Max idle connections 10
Max connection lifetime 5 minutes

Deployment Guide

Docker (single container)

# backend/Dockerfile
FROM golang:1.25-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /aamos-admin ./cmd/main.go

FROM gcr.io/distroless/static:nonroot
COPY --from=builder /aamos-admin /aamos-admin
EXPOSE 8080
ENTRYPOINT ["/aamos-admin"]
docker build -t aamos-admin:latest ./backend

docker run -d \
  --name aamos-admin \
  -p 8080:8080 \
  -e DB_URL="postgres://user:pass@db:5432/aamos?sslmode=disable" \
  -e JWT_SECRET="$(openssl rand -base64 48)" \
  -e CORS_ORIGINS="https://admin.example.com" \
  aamos-admin:latest

Docker Compose (backend + database)

services:
  db:
    image: postgres:15-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: aamos
      POSTGRES_USER: aamos
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U aamos"]
      interval: 5s
      timeout: 3s
      retries: 5

  backend:
    build: ./backend
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "8080:8080"
    environment:
      DB_URL: postgres://aamos:${DB_PASSWORD}@db:5432/aamos?sslmode=disable
      JWT_SECRET: ${JWT_SECRET}
      CORS_ORIGINS: ${CORS_ORIGINS}
      PORT: "8080"

volumes:
  pgdata:
# .env
DB_PASSWORD=strong-db-password
JWT_SECRET=$(openssl rand -base64 48)
CORS_ORIGINS=https://admin.example.com

docker compose up -d
docker compose logs -f backend

systemd (bare metal / VM)

# /etc/systemd/system/aamos-admin.service
[Unit]
Description=AAMOS Admin Backend
After=network.target postgresql.service

[Service]
Type=simple
User=aamos
WorkingDirectory=/opt/aamos-admin/backend
EnvironmentFile=/etc/aamos-admin/env
ExecStart=/opt/aamos-admin/backend/aamos-admin
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
# /etc/aamos-admin/env
PORT=8080
DB_URL=postgres://aamos:secret@localhost:5432/aamos?sslmode=disable
JWT_SECRET=replace-with-64-char-random-string
CORS_ORIGINS=https://admin.example.com

sudo systemctl daemon-reload
sudo systemctl enable --now aamos-admin
sudo systemctl status aamos-admin

First-run: create admin user

After the backend starts for the first time, create an admin account:

curl -X POST http://localhost:8080/api/v1/users \
  -H "Content-Type: application/json" \
  -d '{
    "email": "admin@example.com",
    "name": "Administrator",
    "role": "admin",
    "password": "change-me-immediately"
  }'

Obtain a token:

TOKEN=$(curl -s -X POST http://localhost:8080/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@example.com","password":"change-me-immediately"}' \
  | jq -r .token)

Verify:

curl http://localhost:8080/health
# {"ok":true,"version":"1.0.0","uptime":"0s"}

iOS — App Store / TestFlight

  1. Product > Archive in Xcode
  2. In Organizer: Distribute App > App Store Connect
  3. Follow the upload wizard — select the provisioning profile for com.aamos.admin
  4. In App Store Connect, submit the build to TestFlight for internal testing

Android — Google Play

cd android
./gradlew :app:bundleRelease
# Output: app/build/outputs/bundle/release/app-release.aab

Sign the AAB with your upload key and upload via the Google Play Console.