# Debian 13 App Server Setup

Panduan ini untuk menyiapkan server kosong Debian 13 sebagai app server QRIS Soundbox Platform.

- App domain: `sms.bizone.id`
- App user: `qrisapp`
- App directory: `/opt/qris-soundbox`
- Env file: `/etc/qris-soundbox/qris-soundbox.env`
- Local app port: `3000`
- MQTT broker: `broker.bizone.id`

Broker MQTT boleh berada di server lain. Dokumen ini fokus ke server aplikasi.

## 1. DNS

Pastikan `sms.bizone.id` sudah mengarah ke public IP server app.

```bash
dig +short sms.bizone.id
curl -4 ifconfig.me
```

## 2. Base Packages

Jalankan sebagai root atau user sudo.

```bash
sudo apt update
sudo apt upgrade -y
sudo apt install -y \
  ca-certificates \
  curl \
  gnupg \
  git \
  openssl \
  build-essential \
  nginx \
  certbot \
  python3-certbot-nginx \
  postgresql \
  postgresql-contrib \
  ufw
```

## 3. Node.js

Gunakan Node.js 22 untuk runtime production.

```bash
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
node -v
npm -v
```

## 4. Firewall

```bash
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status
```

Jangan buka port `3000` ke internet. Traffic publik masuk lewat Nginx.

## 5. App User

Buat user khusus untuk menjalankan service. Semua proses aplikasi, install dependency, build, migration, dan systemd runtime dijalankan sebagai `qrisapp`; user `root`/sudo hanya dipakai untuk install paket, membuat direktori, menulis env, Nginx, dan systemd.

```bash
sudo adduser --system --group --home /opt/qris-soundbox qrisapp
sudo install -d -o qrisapp -g qrisapp -m 750 /opt/qris-soundbox
sudo install -d -o root -g qrisapp -m 750 /etc/qris-soundbox
sudo install -d -o qrisapp -g qrisapp -m 750 /var/lib/qris-soundbox/exports
```

Untuk deploy via git/rsync, operator boleh login dengan user sudo biasa, tetapi command yang menyentuh repo/app dijalankan via `sudo -u qrisapp ...`.

## 6. PostgreSQL

Buat database dan user production.

```bash
DB_PASSWORD="$(openssl rand -hex 32)"
echo "Simpan DB password ini untuk env: ${DB_PASSWORD}"

sudo -u postgres psql -v app_password="$DB_PASSWORD" <<'SQL'
CREATE USER qris_app WITH PASSWORD :'app_password';
CREATE DATABASE qris_soundbox_platform OWNER qris_app;
GRANT ALL PRIVILEGES ON DATABASE qris_soundbox_platform TO qris_app;
\q
SQL
```

Kalau user/database sudah pernah dibuat, gunakan reset password:

```sql
ALTER USER qris_app WITH PASSWORD '<password-baru>';
```

## 7. Deploy Code

Contoh deploy awal via git:

```bash
sudo -u qrisapp git clone https://git.iptek.co/wirabasalamah/Qris-Soundbox.git /opt/qris-soundbox
sudo -u qrisapp bash -lc 'cd /opt/qris-soundbox && npm ci'
sudo -u qrisapp bash -lc 'cd /opt/qris-soundbox && npm run typecheck'
sudo -u qrisapp bash -lc 'cd /opt/qris-soundbox && npm run build'
```

Jika deploy dari artifact, extract/copy artifact ke `/opt/qris-soundbox`, lalu:

```bash
sudo chown -R qrisapp:qrisapp /opt/qris-soundbox
sudo -u qrisapp bash -lc 'cd /opt/qris-soundbox && npm ci'
sudo -u qrisapp bash -lc 'cd /opt/qris-soundbox && npm run build'
```

Catatan: command `npm run typecheck`, `npm run db:migrate`, dan sebagian smoke script membutuhkan dev dependency. Untuk server staging/pilot awal, gunakan `npm ci` penuh agar semua command operasional tersedia. Service production tetap menjalankan hasil build lewat `npm run start:dist`.

## 8. Environment

Buat env production:

```bash
sudo nano /etc/qris-soundbox/qris-soundbox.env
```

Template awal:

```env
NODE_ENV=production
PORT=3000
TRUST_PROXY=true
JSON_BODY_LIMIT=1mb
LOG_FORMAT=json
LOG_LEVEL=info

ADMIN_AUTH_ALLOW_LEGACY_TOKEN=false
ADMIN_DEV_LOGIN_ENABLED=false
ADMIN_SESSION_SECRET=CHANGE_ME_LONG_RANDOM_ADMIN_SESSION_SECRET
ADMIN_SESSION_TTL_SECONDS=28800

MERCHANT_AUTH_ALLOW_LEGACY_TOKEN=false
MERCHANT_DEV_LOGIN_ENABLED=false
MERCHANT_SESSION_SECRET=CHANGE_ME_LONG_RANDOM_MERCHANT_SESSION_SECRET
MERCHANT_SESSION_TTL_SECONDS=28800

DEVICE_AUTH_ALLOW_LEGACY_TOKEN=false
TRACE_HEADER=x-request-id
IDEMPOTENCY_TTL_MS=300000
INTEGRATION_WEBHOOK_SECRET=CHANGE_ME_LONG_RANDOM_WEBHOOK_SECRET

MQTT_PUBLISH_MODE=broker
MQTT_BROKER_URL=mqtts://broker.bizone.id:8883
MQTT_USERNAME=qris-backend
MQTT_PASSWORD=CHANGE_ME_MQTT_BACKEND_PASSWORD
MQTT_CLIENT_ID=qris-platform-backend-prod
MQTT_CONNECT_TIMEOUT_MS=5000
MQTT_SUBSCRIBE_ENABLED=true
MQTT_SUBSCRIBE_TOPICS=devices/+/uplink/#,soundbox/+/up,soundbox/+/down/heartbeat
MQTT_PUBLISH_FORCE_FAIL_ALL=false
MQTT_PUBLISH_FORCE_FAIL_DEVICE_IDS=
MQTT_PUBLISH_DEFAULT_RETRY_INTERVAL_MS=15000

QF100_MQTT_BROKER_HOST=broker.bizone.id
QF100_MQTT_BROKER_PORT=1883
QF100_MQTT_USERNAME=qris-backend
QF100_MQTT_PASSWORD=CHANGE_ME_MQTT_BACKEND_PASSWORD
QF100_MQTT_KEEP_ALIVE_SECONDS=60
QF100_APP_CONFIG_VERSION=21
QF100_DYNAMIC_QR_URL=
QF100_DYNAMIC_QR_GAP_SECONDS=0

DYNAMIC_QR_EXPIRY_SCHEDULER_ENABLED=true
DYNAMIC_QR_EXPIRY_SWEEP_INTERVAL_MS=60000
DYNAMIC_QR_EXPIRY_SWEEP_LIMIT=100

EXPORT_WORKER_ENABLED=true
EXPORT_WORKER_INTERVAL_MS=2000
EXPORT_WORKER_BATCH_SIZE=2
EXPORT_JOB_STALE_RUNNING_MS=900000
EXPORT_SETTLEMENT_ADJUSTMENT_MAX_ROWS=5000
EXPORT_STORAGE_DIR=/var/lib/qris-soundbox/exports
EXPORT_RETENTION_DAYS=30

RATE_LIMIT_ENABLED=true
RATE_LIMIT_LOGIN_WINDOW_MS=60000
RATE_LIMIT_LOGIN_MAX=20
RATE_LIMIT_DEVICE_WINDOW_MS=60000
RATE_LIMIT_DEVICE_MAX=600
RATE_LIMIT_ADMIN_WRITE_WINDOW_MS=60000
RATE_LIMIT_ADMIN_WRITE_MAX=300

FINANCE_PLATFORM_FEE_BPS=70
SETTLEMENT_ADJUSTMENT_REQUIRE_APPROVAL=true

PGHOST=127.0.0.1
PGPORT=5432
PGUSER=qris_app
PGPASSWORD=CHANGE_ME_STRONG_DB_PASSWORD
PGDATABASE=qris_soundbox_platform
DATABASE_URL=postgresql://qris_app:CHANGE_ME_STRONG_DB_PASSWORD@127.0.0.1:5432/qris_soundbox_platform
```

Ganti semua `CHANGE_ME_*` dengan secret production. Untuk generate secret cepat:

```bash
openssl rand -hex 32
```

Lock permission:

```bash
sudo chown root:qrisapp /etc/qris-soundbox/qris-soundbox.env
sudo chmod 640 /etc/qris-soundbox/qris-soundbox.env
```

## 9. Database Migration

```bash
cd /opt/qris-soundbox
sudo -u qrisapp env $(sudo cat /etc/qris-soundbox/qris-soundbox.env | xargs) npm run db:migrate
```

Jika env berisi karakter spesial yang membuat `xargs` bermasalah, gunakan systemd service sementara atau jalankan lewat shell yang melakukan `set -a; source ...; set +a`.

```bash
cd /opt/qris-soundbox
sudo -u qrisapp bash -lc 'set -a; source /etc/qris-soundbox/qris-soundbox.env; set +a; npm run db:migrate'
```

## 10. Systemd Service

Buat service:

```bash
sudo nano /etc/systemd/system/qris-soundbox.service
```

Isi:

```ini
[Unit]
Description=QRIS Soundbox Platform
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
Type=simple
User=qrisapp
Group=qrisapp
WorkingDirectory=/opt/qris-soundbox
EnvironmentFile=/etc/qris-soundbox/qris-soundbox.env
ExecStart=/usr/bin/npm run start:dist
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ReadWritePaths=/opt/qris-soundbox /var/lib/qris-soundbox

[Install]
WantedBy=multi-user.target
```

Start service:

```bash
sudo systemctl daemon-reload
sudo systemctl enable qris-soundbox
sudo systemctl start qris-soundbox
sudo systemctl status qris-soundbox --no-pager
journalctl -u qris-soundbox -f
```

## 11. Nginx

Buat config:

```bash
sudo nano /etc/nginx/sites-available/sms.bizone.id
```

Isi:

```nginx
server {
  listen 80;
  listen [::]:80;
  server_name sms.bizone.id;

  client_max_body_size 1m;

  location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}
```

Enable:

```bash
sudo ln -s /etc/nginx/sites-available/sms.bizone.id /etc/nginx/sites-enabled/sms.bizone.id
sudo nginx -t
sudo systemctl reload nginx
```

## 12. TLS Certificate

```bash
sudo certbot --nginx -d sms.bizone.id
sudo certbot renew --dry-run
```

Jika firmware soundbox memakai config URL non-TLS `http://sms.bizone.id/speaker/dev-config` dan tidak bisa follow redirect ke HTTPS, pastikan Nginx tetap melayani `/speaker/` di port 80. Contoh server block HTTP:

```nginx
server {
  listen 80;
  listen [::]:80;
  server_name sms.bizone.id;

  location /speaker/ {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }

  location / {
    return 301 https://$host$request_uri;
  }
}
```

Portal dashboard tetap pakai HTTPS, tetapi device config API bisa tetap diakses via HTTP khusus path `/speaker/`.

## 13. Create Production Users

```bash
cd /opt/qris-soundbox
sudo -u qrisapp bash -lc 'set -a; source /etc/qris-soundbox/qris-soundbox.env; set +a; npm run admin:create-user -- --email <email> --name <name> --role admin --password <strong-password>'
sudo -u qrisapp bash -lc 'set -a; source /etc/qris-soundbox/qris-soundbox.env; set +a; npm run merchant:create-user -- --merchant <merchant-id-or-code> --email <email> --name <name> --role owner --password <strong-password>'
```

## 14. Verification

```bash
curl -fsS https://sms.bizone.id/health
curl -fsS https://sms.bizone.id/health/deep
curl -I https://sms.bizone.id/
curl -I https://sms.bizone.id/ui
curl -I https://sms.bizone.id/ui/soundbox-ops
```

Expected:

- `https://sms.bizone.id/` redirects to `/ui/soundbox-ops`.
- `https://sms.bizone.id/ui` redirects to `/ui/soundbox-ops`.
- `https://sms.bizone.id/ui/soundbox-ops` returns the Soundbox Ops portal HTML.

Test config endpoint shape. Replace `9011001900006` with a serial number already registered and active in Device Registry.

```bash
curl -fsS https://sms.bizone.id/speaker/dev-config \
  -H 'Content-Type: application/json' \
  -d '{
    "dev-model": "QF100",
    "item-number": "00",
    "dev-sn": "9011001900006",
    "hardware-config": "0x0F",
    "fw-version": "1.1.2",
    "fw-build": 13,
    "app-config-version": 21,
    "imei": "12345678123456789",
    "imsi": "310150123456789",
    "iccid": "898600MFSSYYGXXXXXXP"
  }'
```

Expected successful device config response:

```json
{
  "error-code": 0,
  "mqtt": {
    "broker-ip": "broker.bizone.id",
    "broker-port": 8883,
    "client-id": "<device-id>",
    "user-name": "qris-backend",
    "password": "<mqtt-password>",
    "subscribe-topic": "soundbox/<dev-sn>/down",
    "publish-topic": "soundbox/<dev-sn>/up",
    "keep-alive": 60
  }
}
```

Run production preflight from server:

```bash
cd /opt/qris-soundbox
sudo -u qrisapp bash -lc 'set -a; source /etc/qris-soundbox/qris-soundbox.env; set +a; npm run deploy:check-env'
sudo -u qrisapp bash -lc 'set -a; source /etc/qris-soundbox/qris-soundbox.env; set +a; npm run smoke:mqtt-real'
```

Run full smoke only on staging/controlled environment because it creates and cleans test data:

```bash
sudo -u qrisapp bash -lc 'set -a; source /etc/qris-soundbox/qris-soundbox.env; set +a; npm run smoke:e2e'
sudo -u qrisapp bash -lc 'set -a; source /etc/qris-soundbox/qris-soundbox.env; set +a; npm run ui:qa'
```

## 15. QF100 Notes

For QF100 devices, app config server URL should point to:

```text
https://sms.bizone.id/speaker/dev-config
```

The config response will point the device to MQTT broker:

```text
broker.bizone.id:8883
```

If the firmware still has MQTT TLS disabled, either patch firmware TLS or prepare a restricted non-TLS pilot listener. Do not expose unrestricted non-TLS MQTT to the internet.

## 16. Routine Ops

Useful commands:

```bash
sudo systemctl status qris-soundbox --no-pager
journalctl -u qris-soundbox -n 200 --no-pager
sudo nginx -t
sudo systemctl reload nginx
```

Backup:

```bash
cd /opt/qris-soundbox
sudo -u qrisapp bash -lc 'set -a; source /etc/qris-soundbox/qris-soundbox.env; set +a; npm run backup:production -- --out /var/backups/qris --include-mosquitto'
```
