# Phase 1 Technical Plan — Laravel API + Next.js Frontend + Inertia Dashboard

Stack: **Laravel** (JSON API, admin dashboard, youth personal dashboard, auth) · **Next.js** (public landing + content pages, hosted separately e.g. Vercel) · **Inertia.js + React** (for the Laravel-hosted dashboards only, which are behind login and don't need SEO). UI copy is Swahili throughout; this document is the technical reference for building it.

---

## 0. Why This Split, and What It Solves

This replaces the earlier single-app Inertia design specifically to solve the SEO/link-preview problem without needing Inertia SSR running as a persistent Node process on cPanel:

- **Next.js handles every page that needs to be crawled or unfurled into a link-preview card** (landing, content browse, content detail, series pages) — using Static Generation / Incremental Static Regeneration, so most requests serve a pre-built static page rather than rendering on every hit. Hosted on a platform built for this (e.g. Vercel), sidestepping cPanel's RAM/persistent-process limits entirely for this part.
- **Laravel no longer needs SSR at all.** Everything left in Laravel/Inertia — the admin dashboard and the youth personal dashboard — is behind login, never crawled, never shared as a link. Plain client-side-rendered Inertia is perfectly fine there, which means Laravel can run as an ordinary PHP app on cPanel with no Node process required on that side either.
- **Trade-off, stated plainly:** this is two codebases now, not one. Laravel exposes a JSON API; Next.js consumes it. That's real added complexity (auth token flow, CORS, a content-revalidation webhook) in exchange for solving the hosting/SEO problem cleanly. Worth it here specifically because sharing and discoverability are core to the product's purpose.

**Assumption made below, flagged as requested:** you said "Laravel for API and dashboard uploading data" and "Next.js for landing page and the content pages." I've interpreted "dashboard" as covering _both_ the admin content-upload dashboard _and_ the youth's personal stats dashboard (points/badges/streaks) — keeping all logged-in, non-public screens together in Laravel/Inertia, since none of them need SEO and splitting auth across three surfaces instead of two would add complexity for no benefit. If you actually want the youth dashboard in Next.js too (e.g. for a unified visual feel with the public site), that's a valid alternative — flag it and I'll restructure this section.

---

## 1. Site Map

### Next.js — Public (no login required, hosted separately from Laravel)

| Route                  | Purpose                                                                                                                   | Rendering                                                                                                   |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `/`                    | Landing page — hero, featured series, latest content, CTA to browse or register                                           | Static, revalidated on publish                                                                              |
| `/content`             | Browse/search all content — filters by type, series, tag, channel                                                         | Static shell + client-side filtering, or ISR                                                                |
| `/content/[slug]`      | Single content detail page — media, caption, share action, tags, related content. Target of trackable share links (`?s=`) | ISR — this is the page that needs the best SEO/Open Graph tags, since it's what gets shared                 |
| `/series`              | List of all series/topics                                                                                                 | Static, revalidated on publish                                                                              |
| `/series/[slug]`       | Series detail — weeks/lessons in order                                                                                    | ISR                                                                                                         |
| `/login` , `/register` | Auth forms, submitting to the Laravel API                                                                                 | Client-rendered — no SEO need, but kept here for a smooth anonymous-browse → share → prompted-to-login flow |

**Key decision, unchanged from before:** all browsing/reading stays public, no login wall. Login is required only to share (and get tracked credit) and to reach the personal dashboard.

### Laravel + Inertia — Youth Dashboard (authenticated, behind login)

| Route               | Purpose                                                                                   |
| ------------------- | ----------------------------------------------------------------------------------------- |
| `/dashboard`        | Personal stats — points, current streak, badges, total confirmed shares, link-click count |
| `/dashboard/shares` | History of content shared, with per-share link and click stats                            |
| `/dashboard/badges` | Badge collection, earned + locked                                                         |
| `/settings`         | Profile (name, phone, church), notification preferences                                   |

### Laravel + Inertia — Admin

| Route                                                                 | Purpose                                                            |
| --------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `/admin`                                                              | Overview — content count, shares this week, top-performing content |
| `/admin/content`, `/admin/content/create`, `/admin/content/{id}/edit` | Content CRUD                                                       |
| `/admin/series`                                                       | Series/topic CRUD, ordering lessons into weeks                     |
| `/admin/tags`                                                         | Tag management                                                     |
| `/admin/channels`                                                     | Channel management (mostly seeded once)                            |
| `/admin/users`                                                        | View registered youth, filter by church                            |

### Laravel — JSON API (consumed by Next.js and by the Inertia dashboards)

| Endpoint (representative)                                                          | Purpose                                                                                   |
| ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `GET /api/content`, `GET /api/content/{slug}`                                      | Public content data for Next.js pages                                                     |
| `GET /api/series`, `GET /api/series/{slug}`                                        | Public series data                                                                        |
| `GET /api/search`                                                                  | Search across content                                                                     |
| `POST /api/share-links`                                                            | Create a trackable share link (auth required)                                             |
| `POST /api/share-events`                                                           | Confirm a share happened (auth required)                                                  |
| `POST /api/link-clicks`                                                            | Log a click on a `?s=` link (public, no auth)                                             |
| `POST /api/auth/login`, `POST /api/auth/register`, `GET /api/auth/google/callback` | Auth, issuing a token Next.js stores                                                      |
| `GET /api/me/stats`                                                                | Personal dashboard data (also usable if any of the dashboard ever moves to Next.js later) |
| `POST /api/webhooks/revalidate` _(outbound, Laravel → Next.js, not inbound)_       | See §3 — Laravel calls Next.js's revalidation endpoint when content is published/edited   |

---

## 2. Database Schema

_(Unchanged from the original design — the schema doesn't care which frontend renders it.)_

### `churches`

`id, name, location (nullable), created_at`

### `users`

`id, name, email (unique), phone (nullable — collected post-signup, not used for login), church_id (FK, nullable), password (nullable — null when the account is Google-only), google_id (nullable, unique), role (enum: youth, admin), push_subscription_id (FK, nullable), created_at`

### `series`

`id, title, slug, description, cover_image, created_at`

### `content`

`id, title, slug, caption (text), type (enum: poster, carousel, reel, video, story), series_id (FK, nullable), week_number (nullable int), status (enum: draft, published), published_at, created_by (FK users), created_at`

### `content_media`

`id, content_id (FK), file_path, order (int), media_type (image/video)`

### `tags` / `content_tag` (pivot)

Standard tag table + pivot.

### `channels`

`id, name (Instagram, TikTok, WhatsApp, Facebook, Telegram, X, YouTube, Snapchat...), slug, icon`

### `content_channel` (pivot)

`content_id, channel_id` — the "supported channels" metadata from the admin upload form.

### `share_links`

`id, code (unique, short — e.g. 8-char base62), user_id (FK), content_id (FK), channel_id (FK, nullable), created_at`

### `share_events`

`id, share_link_id (FK), confirmed_at` — counts as a share for points/streaks on send-confirmation, not proof of delivery.

### `link_clicks`

`id, share_link_id (FK), visitor_hash (hashed IP+UA), clicked_at`

### `points_ledger`

`id, user_id (FK), points (int), reason (enum: confirmed_share, streak_bonus, badge_bonus), reference_type, reference_id, created_at`

### `badges`

`id, key (unique string), name, description, icon, criteria_type (enum: first_share, content_type_variety, share_count_threshold, streak_threshold), criteria_value (json)`

### `user_badges`

`id, user_id (FK), badge_id (FK), earned_at`

### `streaks`

`id, user_id (FK), current_streak (int), longest_streak (int), last_active_period (date), period_type (enum: week)`

### `push_subscriptions`

`id, user_id (FK), endpoint, p256dh_key, auth_key, created_at`

---

## 3. Cross-App Mechanics — What Changes With the Split

### 3.1 Content revalidation (Laravel → Next.js)

Because content pages are statically generated/ISR'd in Next.js, Next.js needs to know when to regenerate them. When an admin publishes or edits content in the Laravel dashboard, Laravel fires a webhook call to Next.js's on-demand revalidation endpoint (`res.revalidate()` / the App Router equivalent), passing a shared secret token, so the relevant `/content/[slug]` (and the `/content` list, `/series/[slug]`) page regenerates immediately rather than waiting for a timed revalidation window.

### 3.2 Trackable links — `?s=` — now split across two apps

**Flow:**

1. Logged-in user on the Next.js `/content/[slug]` page taps "Share." A client component calls `POST {LARAVEL_API}/api/share-links` with the user's auth token and `content_id`.
2. Laravel creates the `share_links` row, returns `https://yourdomain.com/content/{slug}?s={code}` (the Next.js domain, not the Laravel one — the link always points at the public site).
3. Frontend invokes the Tier 2 hand-off (`navigator.share()` / native share-intent) with that URL + caption + media.
4. On return, the "Umeshiriki? ✅" button fires `POST {LARAVEL_API}/api/share-events` — same as before, this is what counts for points/streaks.
5. **Click logging now happens differently than in the single-app design.** The `/content/[slug]` page is statically generated, so it can't run server-side middleware per-request the way the old Laravel-only design did. Instead: a small client-side effect on the content page reads the `s` search param on mount and fires `POST {LARAVEL_API}/api/link-clicks` in the background. This keeps the page itself fully static/cacheable while still logging every visit that carries a share code — the trade-off is that clicks from users with JavaScript disabled won't be counted, which is a reasonable one given the audience.

**`share_events` vs. `link_clicks` stays a deliberate two-table split** for the same reason as before — self-reported shares and indirect reach data are shown separately, never blended, per the Phase 1 honesty note.

### 3.3 Auth token flow across two domains

Laravel issues a token on login (Sanctum, or a simple signed JWT-style token) via `/api/auth/*`. Next.js stores it (httpOnly cookie is preferable to localStorage for security, but requires Laravel and Next.js to share a parent domain — see below) and attaches it as a Bearer token on subsequent API calls (share-link creation, share-event confirmation, personal stats).

**Recommended domain structure to keep this simple:** put Next.js on the root domain (`yourdomain.com`) and Laravel on a subdomain (`api.yourdomain.com` for the API, or `app.yourdomain.com` if you want the Inertia dashboards reachable directly by URL too). Same-parent-domain subdomains make cookie-based auth and CORS meaningfully easier than two fully unrelated domains would.

---

## 4. Gamification — Built to Extend

_(Unchanged — this logic lives entirely in Laravel regardless of which frontend calls it.)_

### Points

- `points_ledger` table — every award is a logged, reasoned event, not a single counter.
- Point values live in a `point_rules` table, not hardcoded, so the economy can be tuned without a deploy.
- v1: points for a confirmed share; a streak-maintenance bonus; a first-time-per-content-type bonus.

### Streaks — "not necessarily daily"

- `period_type = week` for v1, matching the weekly lesson cadence.
- `period_type` exists specifically so the rule can loosen or tighten later without a schema change.
- Evaluated via a scheduled weekly job.

### Badges

- Criteria-driven (`criteria_type` + `criteria_value`), evaluated after every confirmed `share_event`.
- Starter set: first share; first week active; per-content-type variety; tiered share-count badges; streak-length badges.

---

## 5. Push Notifications

- Web Push (VAPID) — package: `laravel-notification-channels/webpush`, still owned by Laravel since it's tied to the authenticated user record.
- Permission request should be contextual (e.g. right after a first confirmed share), not on page load.
- **iOS Safari limitation unchanged:** web push only works if the site is installed to the home screen as a PWA. Since the public site is now a separate Next.js app from the dashboard, decide which one (if either) you'd want installable — likely the Next.js public site, since that's where most youth traffic lands first.

---

## 6. Admin Content Upload (Laravel + Inertia)

Form fields:

- Title, caption (default share text)
- Type (poster / carousel / reel / video / story) — determines the upload widget
- Series + week number
- Tags (multi-select, create-new inline)
- **Supported channels** (checkboxes against `channels`, stored via `content_channel`) — surfaced later on the Next.js content page so the share UI only offers approved channels
- Status (draft/published), optional `published_at`
- **New with this split:** on save/publish, Laravel fires the revalidation webhook to Next.js (§3.1) so the change appears on the public site immediately rather than waiting for the next scheduled rebuild

Recommended package: `spatie/laravel-medialibrary`.

---

## 7. Registration & Auth

**Decision, unchanged:** email/password plus "Login with Google," via Laravel Breeze/Fortify + `laravel/socialite`. No SMS/OTP cost.

**What changes with the split:** these auth screens now render as Next.js pages (per your site map request) rather than Inertia pages, submitting to Laravel's `/api/auth/*` endpoints and receiving a token back (§3.3) instead of Laravel setting an Inertia session directly. Google's OAuth callback still happens server-side in Laravel; Next.js redirects into it and receives the user back with a token.

Google signups still land on a **"Kamilisha Wasifu Wako"** (complete your profile) step to collect phone and church, since Google doesn't supply those.

---

## 8. Search & Tags

- Search across title, caption, tag names, series title — served via the Laravel API (`GET /api/search`) and consumed by the Next.js `/content` page.
- Recommended: Laravel Scout, database driver for Phase 1, swappable to Meilisearch later via config.
- Filters (type, series, tag, channel) — same query logic, now returned as JSON instead of Inertia props.

---

## 9. Suggested Package / Tooling List

| Purpose                                         | Choice                                                                                               |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| Media uploads (carousels, ordering, thumbnails) | `spatie/laravel-medialibrary`                                                                        |
| Tags                                            | `spatie/laravel-tags`                                                                                |
| Slugs                                           | `spatie/laravel-sluggable`                                                                           |
| Search                                          | `laravel/scout` (database driver → Meilisearch later)                                                |
| Push notifications                              | `laravel-notification-channels/webpush`                                                              |
| API auth tokens                                 | `laravel/sanctum` (token mode, not just SPA cookie mode, given the cross-domain setup)               |
| Email/password auth scaffolding                 | `laravel/breeze` or `laravel/fortify`                                                                |
| Google login                                    | `laravel/socialite`                                                                                  |
| Inertia route helpers (dashboard only)          | `tightenco/ziggy`                                                                                    |
| Next.js data fetching                           | Native `fetch` with ISR (`revalidate` option) against the Laravel API                                |
| Next.js hosting                                 | Vercel (or any Next.js-capable host) — deliberately separate from the cPanel account running Laravel |

---

## 10. Open Decisions Before Building

1. **Confirm the "dashboard" interpretation from §0** — youth personal dashboard staying in Laravel/Inertia alongside admin, rather than moving to Next.js.
2. **Domain structure** — root domain for Next.js, subdomain for Laravel API/dashboards (recommended in §3.3), or another arrangement.
3. **iOS push fallback** — WhatsApp/SMS reminders as a substitute where PWA install isn't realistic.
4. **Click de-duplication on `link_clicks`** — logging every hit is simplest for Phase 1; per-visitor daily dedupe can be added later without a schema change.

Want to confirm the dashboard/domain assumptions first, or move on to scaffolding the actual Next.js content-page data-fetching code?
