Admin Guide
This guide covers setting up and managing the Aquoris Payment service. Steps are the same for staging and production unless noted.
| Staging | Production | |
|---|---|---|
| URL | https://aquoris-payment-staging.eidy.io | https://payment.aquoris.ai |
| Stripe mode | Sandbox (test keys, test cards) | Live (real payments) |
| Deploy trigger | Automatic on merge to main | Manual via GitHub Actions (approval required) |
| Compose file | docker/docker-compose.staging.yml | docker/docker-compose.prod.yml |
First-Time Setup
After deploying the service and running migrations, you need to create the initial data via the admin panel.
1. Create Your Admin Account
On first deploy, the seed script creates a default admin. Run it via the toolbox container:
docker run --rm --network aquoris-payment_internal --env-file .env \
ghcr.io/eidy-trillionsure/aquoris-payment-toolbox:latest \
db:seed
The seed is idempotent (uses upserts) and safe to re-run. It does not reset existing admin passwords.
2. Log In to Admin Panel
Go to https://{your-domain}/admin/login and sign in with the admin credentials.
Rate limit: Login is limited to 5 attempts per minute per IP in production (50/min in development).
3. Create Apps
Navigate to Admin > Apps and create each Aquoris app that will use billing:
- Slug: A stable identifier (e.g.,
vega). Client apps use this in API calls. Max 128 characters. - Name: Display name (e.g.,
Vega). Max 1024 characters. - Return URL: Where to redirect users after billing. Must use
httporhttps.
An API key is generated automatically. Copy it from the table and configure it in the client app.
| Staging | Production | |
|---|---|---|
| Return URL example | https://staging.vega.aquoris.ai/settings | https://vega.aquoris.ai/settings |
4. Set Up Stripe
Each environment uses a separate Stripe account or mode:
| Staging | Production | |
|---|---|---|
| Stripe mode | Sandbox (test mode) | Live |
| Keys prefix | sk_test_... / whsec_... | sk_live_... / whsec_... |
| Test cards | 4242 4242 4242 4242 works | Real cards only |
Create Products in Stripe Dashboard
- Go to Stripe Dashboard > Product catalog > Add product
- For each paid plan, create a product with a recurring price
- Copy the Price ID (starts with
price_...)
Do this in both Stripe sandbox and live mode. Each environment gets its own Price IDs.
Create Plans in Admin Panel
Navigate to Admin > Plans and create your plans:
| Field | Free Plan | Paid Plan |
|---|---|---|
| Slug | free | pro |
| Name | Aquoris Free | Aquoris PRO |
| Price (cents) | 0 | 29900 |
| Currency | thb | thb (or usd) |
| Interval | month | month |
| Stripe Price ID | (leave empty) | price_... from Stripe |
The Stripe Price ID is different per environment — use the sandbox price for staging and the live price for production.
Configure Webhook
- Go to Stripe Dashboard > Developers > Webhooks > Add endpoint
- URL:
- Staging:
https://aquoris-payment-staging.eidy.io/api/v1/webhooks/stripe - Production:
https://payment.aquoris.ai/api/v1/webhooks/stripe
- Staging:
- Select these events:
checkout.session.completedcustomer.subscription.updatedcustomer.subscription.deletedinvoice.paidinvoice.payment_failedcharge.refunded
- Copy the Signing secret (
whsec_...) - Add it to the server's
.envasSTRIPE_WEBHOOK_SECRET
Create a separate webhook endpoint for each environment. Each gets its own signing secret.
5. Test the Flow
- Create a billing session via API (use the app's API key from Admin > Apps):
curl -X POST https://{your-domain}/api/v1/sessions \ -H "Authorization: Bearer YOUR_APP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"externalUserId":"test-user","appSlug":"vega"}' - Visit the returned URL — you should see the billing dashboard
- Click "Subscribe" on a paid plan — should redirect to Stripe Checkout
- Staging only: Use test card
4242 4242 4242 4242(any expiry, any CVC) - After payment, the webhook should fire and create the subscription
Ongoing Management
Managing Subscriptions
- View all subscriptions: Admin > Subscriptions (filter by status). Shows exact total count with infinite scroll.
- Cancel a subscription: Customer detail > Cancel Sub (cancels at period end)
- Extend a subscription: Customer detail > Extend (add days + reason)
- Grant a manual subscription: Customer detail > Grant Subscription
Manual Payments
For payments received outside Stripe (bank transfer, wire):
- Go to Customer detail > Record Payment
- Enter amount, currency (THB/USD), payment method, reference number, plan, and duration
- This creates an Invoice (source: MANUAL) and a Subscription
Refunds
- Stripe invoices: Admin issues refund > Stripe processes it > webhook confirms
- Manual invoices: Admin records refund > bookkeeping entry only
- Immediate cancellation: When issuing a refund, check "Also cancel subscription immediately" to terminate the subscription right away instead of at period end. If the refund succeeds but the cancellation fails, a warning is shown and the refund is still recorded.
Monitoring
- Dashboard: Admin > Dashboard shows MRR, active subscriptions, churn rate, revenue. Stats have a 10-second timeout — if computation takes too long, cards show "--".
- Audit log: Admin > Audit Log shows all actions with timestamps, actors, and details. Supports infinite scroll.
- Health check:
GET /api/healthreturns database connectivity status (200 = ok, 503 = degraded). Sentry uptime monitoring is configured for production. - Error tracking: Sentry captures errors across client, server, and background jobs. Each environment reports to its own Sentry project.
Input Validation
All admin forms enforce field length limits:
| Field | Max Length |
|---|---|
| Name (admin, app) | 1024 characters |
| Plan slug | 128 characters |
| Plan name | 1024 characters |
| Password | 72 characters (bcrypt limit) |
| Valid format required | |
| Return URL | Must use http or https scheme |
Required Environment Variables
See the Integration Guide for the full environment variables reference.
Adding New Plans
- Create the product and price in Stripe Dashboard (in both sandbox and live mode)
- Create the plan in Admin > Plans with the correct Stripe Price ID for each environment
- The new plan appears automatically on the billing pricing page
Adding New Apps
- Create the app in Admin > Apps with slug and return URL
- Copy the generated API key
- Configure the client app to use the app slug and API key
Infrastructure Operations
Database Backups
Automatic pre-deploy backups are taken by the CI/CD pipeline before each migration. Manual backups:
# On the server (staging or production)
/opt/aquoris-payment/scripts/backup-db.sh
Backups are stored as gzipped SQL dumps in /opt/aquoris-payment/backups/ with 30-day retention. The script also supports webhook notifications on failure via the WEBHOOK_URL env var.
DigitalOcean daily droplet snapshots provide an additional safety net.
Deploy Pipeline
The GitHub Actions deploy workflow (deploy.yml) runs these steps:
- Pull the new Docker images (app + toolbox)
- Wait for Postgres to be ready
- Take a pre-deploy database backup (verified non-empty)
- Run Prisma migrations via the toolbox container
- Tag the current app image as
:rollback - Deploy the new app image
- Wait up to 60 seconds for the health check to pass
- On failure: retag
:rollbackas:latest, recreate via compose, exit with error - Upload source maps to Sentry via the toolbox container
| Staging | Production | |
|---|---|---|
| Trigger | Automatic on merge to main | Manual via GitHub Actions → Deploy → Run workflow → production |
| Approval | None | Requires environment approval gate |
Restoring from Backup
# On the server — use the correct compose file for the environment
docker compose -f docker/docker-compose.{staging,prod}.yml stop app
# Restore (interactive — prompts for confirmation)
/opt/aquoris-payment/scripts/restore-db.sh /opt/aquoris-payment/backups/payment-YYYYMMDD-HHMMSS.sql.gz
# Restart
docker compose -f docker/docker-compose.{staging,prod}.yml up -d app
Graceful Shutdown
On docker compose stop app, the server:
- Stops accepting new connections
- Waits for in-flight requests to complete
- Stops pg-boss (waits for running jobs to finish)
- Disconnects from the database
The Docker stop grace period is 30 seconds.
Dependency Updates
Dependabot opens PRs weekly for:
- npm dependencies (grouped minor/patch)
- GitHub Actions versions
- Docker base images
Review and merge these regularly to stay on top of security patches.