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