Case study · Web app & installable PWA

Taking a tutoring practice from scattered WhatsApp messages to an installable app.

Épura teaches Descriptive Geometry in Guimarães, Portugal. Before this project, booking lessons and tracking students lived in loose chat messages. The build shipped a public marketing site, a student portal, and a teacher backoffice — all running as one Progressive Web App that installs on iOS and Android home screens with no app store in between.

Client
Épura — Descriptive Geometry tutoring
Stack
Next.js (App Router) Supabase Tailwind CSS PWA Resend

Built from a blank repository

There was no starting codebase. The brief was the brand's existing visual identity — colors, typography, iconography already defined by the client's design team — and everything else was built up from there: routing, database schema, authentication, every page, every form, every email.

Public marketing site

Home, method, services, testimonials, FAQ, legal pages, and a booking flow that captures student, guardian, and billing details in one form.

Student portal

Each student logs in to see their own upcoming and past lessons, edit their profile, and opt into push notifications — backed by real Supabase auth, not a mock.

Teacher backoffice

Full CRUD over students and lessons, a calendar view, an incoming-signups queue, and account settings — the operational core the teacher runs the business from.

Épura app open on a phone home screen, showing the login screen

On the home screen, not in a browser tab

Installed like a native app

This is the actual app, running on a real phone — added to the home screen through the browser's install prompt, opening straight to the login screen with no address bar, no tabs, its own icon next to every other app. The same install flow works on both iOS and Android from one Next.js codebase, no App Store submission required.

Designed by marketing, built to match

The visual identity — palette, typeface, iconography — had already been produced by the client's marketing team before development started. The job was to translate that system faithfully into a working interface: every screen, from the public site to the student portal to the teacher backoffice, pulls from the same tokens, not a developer's reinterpretation of the brand.

#081F2D Primary — navy
#003A5D Secondary — blue
#EBA900 Accent — amber
#FFFFFF Ground — white

Palette and Sora typeface as delivered by the marketing team, applied consistently across all three surfaces.

3
surfaces built end-to-end: public site, student portal, teacher backoffice
2
native install platforms, iOS and Android, no app store in between
2
notification channels wired to the database: email and push, per lesson
1
Supabase project — auth, data, and row-level access rules, live from day one

Wired to a real database, not a demo

Every screen reads and writes live data. Booking a lesson in the backoffice inserts a row, fires a confirmation email through Resend, and pushes a notification to the student's installed app — the same action updates three systems at once. Access is enforced at the database layer with Supabase Row Level Security: a student's queries can only ever return their own rows; only the teacher's account carries admin rights across every student.

Two install bugs worth remembering

Getting a PWA to actually offer installation in production surfaced two failures that don't show up in local development.

01

The install prompt never showed up on Android

Android · Chrome

Root cause

The public domain sat behind a "coming soon" gate for client review before launch. The middleware enforcing that gate excluded images by file extension, but not .js — so /sw.js itself was being redirected for any visitor without the preview cookie. Service worker registration failed silently, and without an active service worker Chrome never offers to install.

Fix

// src/proxy.ts
export const config = {
  matcher: [
    "/((?!_next/static|_next/image|favicon.ico|icon|images/|sw.js|manifest.webmanifest|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
  ],
};
02

Installing on iPhone left the banner stuck on screen

iOS · Safari

Root cause

The click handler dismissed the install banner unconditionally, even on the branch that was only supposed to open the "Add to Home Screen" instructions modal (iOS has no native install prompt). The banner disappeared in the same render pass as the modal — which lived inside the same component — so the instructions never actually got seen.

Fix

// before: dismissed on both branches
function handleInstall() {
  if (hasNativePrompt) promptInstall();
  else if (isIOS) setShowIOSInstructions(true);
  dismiss(); // ← ran every time, killed the modal it just opened
}

// after: only dismiss once the native prompt actually fired
function handleInstall() {
  if (hasNativePrompt) {
    promptInstall();
    dismiss();
  } else if (isIOS) {
    setShowIOSInstructions(true);
  }
}

Backoffice: a schedule that replaces the notebook

The teacher manages students, lessons, and incoming sign-ups without leaving the app — a monthly calendar showing each session's status, a per-student view with the full booking history, and a queue of new sign-ups coming in from the public form. Booking or cancelling a lesson here is also what triggers the student's confirmation email and push notification, so the notebook and the notice to the family are the same action.

See it live

The site is live at epura-explicacoes.pt. Try installing the app from a phone — Chrome offers the native install prompt, Safari walks through adding it to the home screen.