Frontend Apps
All four frontend apps are deployed on Cloudflare Pages. They are static builds — no server-side rendering at runtime. All data fetching happens client-side via the Main API.
Why Cloudflare Pages?
We evaluated Vercel, Netlify, and Cloudflare Pages:
- Cloudflare Pages — zero-cost static hosting with unlimited bandwidth, tight integration with our existing Cloudflare DNS/CDN/Zero Trust setup, and fast global edge deploys
- Vercel — excellent DX but per-seat pricing doesn't make sense for a static app
- Netlify — similar to Vercel, pricing favours dynamic/serverless use cases
Since all our apps are pure SPAs hitting an external API, Cloudflare Pages was the obvious choice.
App Overview
| App | Subdomain | Status | Users |
|---|---|---|---|
| Marketing Website | semalink.africa | Live | Public |
| Customer Web App | app.semalink.africa | Active development | Business customers |
| Internal Admin App | admin.semalink.africa | Planned | Sema Link team |
| Agent Portal | agents.semalink.africa | Planned | Agents / resellers |
Marketing Website
Tech: Vue 3 + vite-ssg (static site generation) Repo: semalink-websiteProduction branch: prod
The public-facing marketing site. Pre-rendered as static HTML at build time using vite-ssg for full SEO indexing without a Node.js server. Vue components author the UI; vite-ssg walks the router and renders each route to an HTML file.
Features:
- Pre-rendered pages for full SEO indexing
- Google Analytics 4 with CTA click event tracking
- Canonical URLs and Open Graph meta tags per route
- Pricing plans, API documentation, company information
Customer Web App
Tech: Vue 3 + Vite (SPA) Repo: semalink-frontendSubdomains: app (prod), staging-app (staging), test-app (test)
The self-service dashboard for business customers.
Technology choices
Vue 3 was chosen over React for this project:
- The team had stronger Vue experience
- Vue's
<script setup>+ Composition API provides excellent TypeScript integration with less boilerplate than React hooks - Pinia (the official Vue state manager) has a simpler API than Redux/Zustand for the scale of this app
Pinia for state management with pinia-plugin-persistedstate for auth token persistence across page refreshes.
Tailwind CSS 4 for styling. v4 drops tailwind.config.js in favour of CSS-based configuration — no config file needed for most projects.
Axios with a custom interceptor that transparently refreshes the access token on 401 without the user noticing. Failed requests are queued during the refresh and retried automatically.
Project structure
src/
├── core/ # App-wide infrastructure
│ ├── api/client.ts # Axios instance with token refresh interceptor
│ ├── guards/ # Vue Router navigation guards
│ ├── rbac/ # Role-based permission helpers
│ └── stores/ # Global stores (toast notifications)
├── features/ # Feature modules (self-contained)
│ ├── auth/ # Login, register, OAuth, password flows
│ ├── contacts/ # Contact & list management
│ ├── dashboard/ # Dashboard widgets
│ └── team/ # Team management
├── views/ # Route-level page components
├── components/
│ └── ui/ # Reusable design system components
└── utils/ # Shared helpers (phone formatting, dates)Design system components
The src/components/ui/ directory contains a lightweight design system built for this project:
| Component | Purpose |
|---|---|
Button | Primary, outline, and ghost variants with loading state |
TextInput | Labelled input with error state, password toggle |
Modal | Accessible dialog with header/footer slots |
Alert | Success, error, warning banners |
SelectMenu | Custom branded dropdown replacing native <select> |
Dropdown / DropdownItem | Menu with outside-click dismiss |
Badge | Status pills |
Tabs | Horizontal tab navigation |
We deliberately avoided a third-party UI library (Vuetify, PrimeVue, etc.) because:
- Third-party component libraries carry significant bundle weight
- Our designs are bespoke — adapting a library to match often takes longer than building
- Tailwind makes building components fast and the result is exactly what the design requires
Axios token refresh flow
The queue prevents multiple simultaneous 401s each triggering their own refresh — only the first fires the refresh, the rest wait and are retried once the new token is available.
Authentication screens (live)
| Route | Component | Notes |
|---|---|---|
/login | Login.vue | Email + password, Google/GitHub/Microsoft/Apple OAuth buttons |
/register | SignUp.vue | Account + user creation |
/forgot-password | ForgotPassword.vue | Sends reset email |
/verify-email-sent | VerifyEmailSent.vue | Post-register instruction screen |
/auth/verify-email | VerifyEmail.vue | Redeems token from email link |
/auth/reset-password | ResetPassword.vue | Sets new password via token |
/auth/oauth/callback | OAuthCallback.vue | Handles OAuth provider redirect |
App screens (live)
| Route | View | Status |
|---|---|---|
/ | DashboardView.vue | Live |
/contacts | ContactsView.vue | Live — full contacts & list management |
/settings | SettingsView.vue | Live — profile, security, notifications |
/team | TeamView.vue | Live — team member management |
/campaign | CampaignView.vue | In progress |
/analytics | AnalyticsView.vue | Placeholder |
/billing | BillingView.vue | Placeholder |
/developer | DeveloperView.vue | Placeholder |
Contacts module — what's built
The contacts module is the most complete feature in the app:
- Contact lists sidebar — create, edit (name + description), delete, with live contact counts
- Contacts table — paginated, searchable, filterable by status
- Add/Edit contact modal — with custom country code picker (searchable, 250+ countries), google-libphonenumber validation with region-aware carrier checking, multi-list assignment, tags
- Bulk actions — select multiple, add to list, export CSV, delete
- Trash — soft-deleted contacts with 90-day retention, restore or purge
- Import — CSV/Excel file upload
- Export — download filtered contacts as CSV
RBAC
Permissions are defined in src/core/rbac/permissions.ts and applied via the useRbac() composable. The role is embedded in the JWT payload and stored in the auth store. UI elements that require elevated roles are conditionally rendered — no separate permission API call needed.
Internal Admin App
Tech: Vue 3 + Vite (SPA) Subdomain: admin.semalink.africaAccess: Cloudflare Access — Sema Link team only Status: Planned
The operational control panel for the Sema Link team. Key capabilities:
- View and manage all customer accounts and credit balances
- Override pricing, apply promotional credits
- Monitor outbound messages and DLR health across the platform
- Manage agents and approve sender IDs
- System health dashboard — RabbitMQ queue depths, SMS Worker throughput, DLR latency
Agent Portal
Tech: Vue 3 + Vite (SPA) Subdomain: agents.semalink.africaAccess: Authenticated agent accounts Status: Planned
Self-service portal for agents (resellers). Agents buy SMS credits from Sema Link at wholesale rates and re-sell at their own margin.
Key capabilities:
- Own credit balance and wholesale rate visibility
- M-Pesa top-up
- Sub-customer management — create accounts, assign pricing, track usage
- Revenue and consumption reporting
See Agent & Reseller Model for the pricing model.