- 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
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
- Architecture
- Prerequisites
- Environment Variables
- Go Backend — Setup & Run
- Rust Core — Build
- Web — Deploy
- iOS — Xcode Setup
- Android — Android Studio Setup
- API Reference
- WebSocket
- Database Schema
- 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 Pro → API 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 (running ↔ stopped).
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
- Product > Archive in Xcode
- In Organizer: Distribute App > App Store Connect
- Follow the upload wizard — select the provisioning profile for
com.aamos.admin - 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.