Skip to content

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

AppSubdomainStatusUsers
Marketing Websitesemalink.africaLivePublic
Customer Web Appapp.semalink.africaActive developmentBusiness customers
Internal Admin Appadmin.semalink.africaPlannedSema Link team
Agent Portalagents.semalink.africaPlannedAgents / 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:

ComponentPurpose
ButtonPrimary, outline, and ghost variants with loading state
TextInputLabelled input with error state, password toggle
ModalAccessible dialog with header/footer slots
AlertSuccess, error, warning banners
SelectMenuCustom branded dropdown replacing native <select>
Dropdown / DropdownItemMenu with outside-click dismiss
BadgeStatus pills
TabsHorizontal 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)

RouteComponentNotes
/loginLogin.vueEmail + password, Google/GitHub/Microsoft/Apple OAuth buttons
/registerSignUp.vueAccount + user creation
/forgot-passwordForgotPassword.vueSends reset email
/verify-email-sentVerifyEmailSent.vuePost-register instruction screen
/auth/verify-emailVerifyEmail.vueRedeems token from email link
/auth/reset-passwordResetPassword.vueSets new password via token
/auth/oauth/callbackOAuthCallback.vueHandles OAuth provider redirect

App screens (live)

RouteViewStatus
/DashboardView.vueLive
/contactsContactsView.vueLive — full contacts & list management
/settingsSettingsView.vueLive — profile, security, notifications
/teamTeamView.vueLive — team member management
/campaignCampaignView.vueIn progress
/analyticsAnalyticsView.vuePlaceholder
/billingBillingView.vuePlaceholder
/developerDeveloperView.vuePlaceholder

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.

Internal use only — Sema Link Engineering