Admin Web App — Overview
The admin portal (semalink-admin) is the internal tool used by Sema Link staff to operate the platform. It is a completely separate application from the customer web app — different repo, different auth system, different deployment pipeline.
Staging: staging-admin.semalink.africaProduction: admin.semalink.africa (not yet live)
Tech Stack
| Layer | Technology |
|---|---|
| Framework | Vite + Vue 3 + TypeScript |
| Styling | Tailwind CSS |
| API | semalink-api — /api/v1/admin/* routes, separate JWT namespace |
| Hosting | Cloudflare Pages |
| CI/CD | GitHub Actions |
Authentication — Two Layers
Staff authentication uses two independent gates, giving effectively 2FA without building TOTP:
Layer 1 — Cloudflare Zero Trust (production only)
- Staff must authenticate via Google SSO before the login page is even reachable
- Access policy restricts to
@semalink.africaemail domain - A stolen password alone is useless without the Google account
Layer 2 — Username/password against the API
- Staff enter their own admin credentials after Zero Trust clears them
- Handled by
POST /api/v1/admin/auth/login - No self-signup. Accounts are created by an owner only via
POST /api/v1/admin/staff - Brute-force protection: rate limiting on the login endpoint + account lockout after N failed attempts
The admin JWT uses a completely separate Fastify namespace (ADMIN_JWT_SECRET) — admin tokens cannot be used on customer routes and vice versa.
Permission Model
Three permission levels per module:
owner > staff | (no access)| Level | Can do |
|---|---|
owner | All actions including destructive/irreversible ones (delete org, manage staff accounts, change pricing) |
staff | Day-to-day operational actions (approve sender IDs, adjust credits, view all data) |
| No access | Module not visible in launcher |
Permissions are stored per staff member on their admin_staff record and carried in the JWT.
Modular Launcher
On login, staff land on a home page showing all modules as cards. Modules with no access are greyed out ("coming soon"). Modules with access are clickable and navigate into that mini-app.
Modules
| Module | API Prefix | Status |
|---|---|---|
| Staff auth | /admin/auth/* | ✅ Login, logout, refresh, change password |
| Staff management | /admin/staff/* | ✅ List, create, update, permissions, activate/deactivate |
| CRM | /admin/crm | ⬜ Org list, user list, suspend/reactivate, impersonate |
| Sender IDs | /admin/sender-ids | ⬜ Approval queue, approve/reject/bulk |
| Billing | /admin/billing | ⬜ Credit balances, ledger, manual top-up/adjustment |
| Pricing | /admin/pricing | ⬜ Per-country SMS rates, agent wholesale rates |
| Agent Manager | /admin/agents | ⬜ Agent list, sub-customer visibility |
| Campaigns | /admin/campaigns | ⬜ Cross-org campaign view |
| Engineering | /admin/engineering | ⬜ Queue depths, DLR backlog, API health |
| Audit Log | /admin/audit | ⬜ Append-only audit log viewer |
First Owner Account
The first staff account (evanson@semalink.africa, role: owner) is seeded directly on the database using a seed script — there is no self-signup flow:
ADMIN_EMAIL=evanson@semalink.africa ADMIN_NAME="Evanson Biwott" \
npx tsx scripts/seed-admin-staff.tsThis has been run on staging. Production seeding happens as part of the production launch checklist.