Case Study
Mecca digital
Complete reference for the **Mecca Digital** link-in-bio platform: what it is, how it is built, how to run it, and how each feature works end-to-end.
Project Details
About This Project
# Mecca Digital — Project Guide
Complete reference for the **Mecca Digital** link-in-bio platform: what it is, how it is built, how to run it, and how each feature works end-to-end.
**Product name:** Mecca Digital
**Repository layout:** Monorepo (`frankin-app` backend + `frontend` SPA)
**Last updated:** 2026-08-02
---
## Table of Contents
1. [What Is This Project?](#1-what-is-this-project)
2. [Repository Structure](#2-repository-structure)
3. [Technology Stack](#3-technology-stack)
4. [Architecture Overview](#4-architecture-overview)
5. [How It Works (End-to-End)](#5-how-it-works-end-to-end)
6. [Database Model](#6-database-model)
7. [Backend API Reference](#7-backend-api-reference)
8. [Frontend Routes & Pages](#8-frontend-routes--pages)
9. [Feature Deep Dives](#9-feature-deep-dives)
10. [Local Development Setup](#10-local-development-setup)
11. [Docker & Services](#11-docker--services)
12. [Environment Variables](#12-environment-variables)
13. [Testing](#13-testing)
14. [Demo Account & Seed Data](#14-demo-account--seed-data)
15. [Key Source Files](#15-key-source-files)
16. [Implementation Status](#16-implementation-status)
17. [Troubleshooting](#17-troubleshooting)
18. [Related Documents](#18-related-documents)
---
## 1. What Is This Project?
**Mecca Digital** is a **link-in-bio** platform (similar to Linktree) with extra **bento-style widgets**, portfolio gallery, file uploads, themes, and analytics.
### Problem it solves
Social platforms allow only **one link** in a bio. Creators need a single URL that opens a mini landing page with:
- Multiple outbound links (GitHub, shop, blog, calendar, etc.)
- A profile header (avatar, name, bio)
- Optional video, résumé, portfolio projects
- Rich widget blocks (stats, gallery, testimonials)
- Branded themes
### Public URL pattern
| Environment | Public profile URL | API base |
|-------------|-------------------|----------|
| Local (Vite) | `http://localhost:8080/{slug}` | `http://localhost:8002/api` |
| Production (planned) | `https://meccadigital.com/{slug}` | `https://api.meccadigital.com/api` |
Example: `http://localhost:8080/smoke817393`
### Target users
- Creators, developers, freelancers who want one shareable link for all their content
- Teams building a Linktree-style SaaS with richer widgets than basic link lists
---
## 2. Repository Structure
```
bento/
├── docs/
│ ├── LINK_IN_BIO_PRODUCT_SPEC.md # Product requirements & phases
│ └── PROJECT_GUIDE.md # This file
├── frankin-app/ # Laravel 12 API backend
│ ├── app/
│ │ ├── Http/Controllers/Api/ # REST API controllers
│ │ └── Models/ # Eloquent models
│ ├── database/
│ │ ├── migrations/
│ │ └── seeders/
│ ├── routes/api.php
│ ├── docker-compose.yml
│ ├── build-and-run.ps1 # Docker helper (Windows)
│ └── tests/e2e-full.ps1 # API + proxy smoke tests
└── frontend/ # React SPA (Vite)
├── src/
│ ├── pages/web/ # Marketing + public profile
│ ├── pages/app/ # Creator dashboard
│ ├── pages/auth/ # Login, signup, reset
│ ├── pages/onboarding/ # Post-signup wizard
│ ├── lib/ # API clients & services
│ └── components/ # UI components
├── vite.config.ts # Dev server + API proxy
└── .env # VITE_API_URL
```
---
## 3. Technology Stack
### Backend (`frankin-app`)
| Layer | Technology |
|-------|------------|
| Framework | Laravel 12 |
| Runtime | PHP 8.4 |
| App server | FrankenPHP + Laravel Octane (default local profile) |
| Alternative | PHP-FPM + Nginx (`php-fpm` Docker profile) |
| Database | PostgreSQL 16 |
| Cache / sessions | Redis 7 |
| Auth | Session cookies (Laravel `web` guard) |
| File storage | Local `public` disk (`storage/app/public`) |
| Admin (legacy) | Livewire + Flux UI (separate from React SPA) |
### Frontend (`frontend`)
| Layer | Technology |
|-------|------------|
| Framework | React 18 |
| Build | Vite 5 |
| Language | TypeScript |
| Routing | React Router 6 |
| Styling | Tailwind CSS 3 + shadcn/ui |
| Icons | Lucide React |
| State / data | TanStack Query, Zustand (partial) |
| Dark mode | next-themes |
| Fonts | Inter |
### Infrastructure (local Docker)
| Service | Container | Host port |
|---------|-----------|-----------|
| FrankenPHP | `frankin-app-frankenphp` | 8002 (HTTP), 8445 (HTTPS) |
| PostgreSQL | `frankin-app-postgres` | 5434 |
| Redis | `frankin-app-redis` | 6380 |
| Vite (optional in Docker) | `frankin-app-node` | 5175 |
| pgAdmin (optional) | `frankin-app-pgadmin4` | 5054 |
---
## 4. Architecture Overview
```mermaid
flowchart TB
subgraph Browser
SPA[React SPA :8080]
end
subgraph Backend
FP[FrankenPHP / Octane :8002]
API[Laravel API routes]
DB[(PostgreSQL)]
RD[(Redis)]
FS[storage/app/public]
end
SPA -->|fetch /api/* credentials:include| FP
FP --> API
API --> DB
API --> RD
API --> FS
Visitor[Visitor] -->|GET /{slug}| SPA
SPA -->|GET /api/profiles/{slug}| FP
```
### Request flow (authenticated dashboard)
1. User logs in via `POST /api/login` → Laravel sets **session cookie**
2. React sends `credentials: "include"` on every API call
3. Dashboard pages call service modules in `frontend/src/lib/*`
4. Laravel validates session via `auth` middleware
### Request flow (public profile)
1. Visitor opens `http://localhost:8080/{slug}`
2. React loads `PublicProfile.tsx` → `GET /api/profiles/{slug}` (no auth)
3. Response includes profile, links, theme, blocks, projects, social links
4. Page view recorded via `POST /api/profiles/{slug}/view`
5. Link clicks go through `GET /api/r/{linkId}` (tracks click, then redirects)
---
## 5. How It Works (End-to-End)
### 5.1 Sign up & onboarding
```mermaid
flowchart LR
A[Signup /signup] --> B[Register API]
B --> C[Profile + slug created]
C --> D[Onboarding socials]
D --> E[Onboarding content]
E --> F[Finish + publish]
F --> G[Dashboard]
```
1. **Signup** (`/signup`): user claims a slug + email/password
2. **Register** (`POST /api/register`): creates `users` + `profiles` row, assigns default theme
3. **Onboarding**: social links, avatar/video upload (multipart)
4. **Complete** (`POST /api/onboarding/complete`): sets `profiles.is_published = true`
5. User lands in **dashboard** at `/dashboard`
### 5.2 Public link page assembly
When someone visits `/{slug}`, the page is built from several data sources:
| Section | Source | Dashboard to edit |
|---------|--------|-------------------|
| Header (avatar, name, bio) | `profiles` | Profile |
| Intro video | `profiles.video_url` | Files (video tab) or onboarding |
| Download résumé | `profiles.resume_url` | Files (resume tab) |
| Link buttons | `links` table | Links |
| Featured link cards | `links` with `is_featured` + `thumbnail_url` | Links |
| Portfolio grid | `projects` table | Gallery |
| Bento widgets | `blocks` on default `pages` row | Widgets |
| Social chips | `social_links` | Profile / Connect GitHub |
| Theme colors | `profiles.theme_id` → `themes` | Appearance |
### 5.3 Themes — save & apply
**Saving (dashboard):**
1. User picks a theme on `/dashboard/appearance`
2. Clicks **Save theme** → `PUT /api/profile` with `{ "theme_id": 3 }`
3. Stored as `profiles.theme_id` (foreign key to `themes` table)
**Applying (public page):**
1. `GET /api/profiles/{slug}` returns `theme: { id, name, config }`
2. Frontend calls `resolvePublicTheme(profile.theme)` in `frontend/src/lib/publicTheme.ts`
3. Theme **name** (Classic, Midnight, Sunset, Minimal, Neon) maps to hardcoded Tailwind classes
4. Classes are applied to page background, link buttons, cards, widgets, text, social chips
> **Important:** Theme CSS classes must exist in frontend **source files** so Tailwind JIT includes them. Database `config` JSON is metadata; rendering uses `PUBLIC_THEME_PRESETS` keyed by theme name.
### 5.4 Links & click tracking
- **CRUD:** `/dashboard/links` → `/api/links`
- **Reorder:** drag-and-drop → `POST /api/links/reorder` with `{ "ids": [3,1,2] }`
- **Featured thumbnails:** optional `thumbnail_url` + `is_featured` for Linktree-style cards
- **Public click:** href points to `/api/r/{id}` → records `link_clicks` → HTTP redirect to destination URL
### 5.5 Widgets (bento blocks)
Blocks belong to a profile's default **page** (`pages.slug = 'main'`).
| Block type | Content shape | Public render |
|------------|---------------|---------------|
| `hero` | `{ title, subtitle, image_url }` | Wide banner card |
| `stats` | `{ items: [{ label, value }] }` | 3-column stat row |
| `gallery` | `{ images: [{ url, caption }] }` | 2×2 image grid |
| `testimonials` | `{ items: [{ quote, author }] }` | Quote card |
Dashboard: `/dashboard/widgets` — add, drag-reorder, inline edit, delete.
### 5.6 Files (unified uploads)
`/dashboard/files` replaces separate Resume / Documents / Videos pages.
| Tab | `kind` | Max size | Notes |
|-----|--------|----------|-------|
| Images | `image` | 5 MB | jpeg, png, webp, gif |
| Videos | `video` | 100 MB | mp4, webm |
| Documents | `document` | 10 MB | pdf, doc, docx |
| Resume | `resume` | 10 MB | Replaces previous; sets `profiles.resume_url` |
API: `POST /api/media` (multipart), `GET /api/media`, `DELETE /api/media/{id}`
Files stored at `storage/app/public/media/{profileId}/{kind}/` and served via `/storage/...`.
### 5.7 Gallery (portfolio projects)
`/dashboard/portfolio` — add projects with cover image (upload or URL), title, description, optional link.
API: `/api/projects` CRUD + reorder.
Public page shows a **Portfolio** grid below links.
### 5.8 Analytics
- **Page views:** `page_views` table, recorded on public profile load
- **Link clicks:** `link_clicks` table, recorded on `/api/r/{link}` redirect
- **Dashboard:** `/dashboard/analytics` → `GET /api/analytics/summary`
---
## 6. Database Model
### Core entities
```mermaid
erDiagram
users ||--o| profiles : has
profiles ||--o{ links : has
profiles ||--o{ social_links : has
profiles ||--o{ projects : has
profiles ||--o{ pages : has
profiles ||--o{ media : has
profiles }o--|| themes : uses
pages ||--o{ blocks : contains
links ||--o{ link_clicks : tracks
pages ||--o{ page_views : tracks
```
### Important tables
| Table | Purpose |
|-------|---------|
| `users` | Auth (email, password, name) |
| `profiles` | Public identity: slug, bio, avatar, video, resume, theme_id, is_published |
| `themes` | Preset themes (name + JSON config) |
| `links` | Link buttons: title, url, thumbnail_url, is_featured, position, is_active |
| `social_links` | Platform + username pairs |
| `pages` | Multi-page support; default `main` page holds blocks |
| `blocks` | Widget JSON content + type + position |
| `projects` | Portfolio items: title, description, url, cover_url, position |
| `media` | Uploaded files: url, kind, mime, size |
| `page_views` | Analytics: profile/page views |
| `link_clicks` | Analytics: per-link click counts |
### Profile slug rules
- 5–100 characters
- Lowercase letters, numbers, hyphens only: `[a-z0-9-]+`
- Checked via `GET /api/profiles/check-slug?slug=...`
---
## 7. Backend API Reference
Base URL (local): `http://localhost:8002/api`
### Public (no auth)
| Method | Path | Description |
|--------|------|-------------|
| GET | `/profiles/check-slug` | Slug availability |
| GET | `/profiles/{slug}` | Full public profile payload |
| POST | `/profiles/{slug}/view` | Record page view |
| GET | `/themes` | List theme presets |
| GET | `/creators` | Explore page creators |
| GET | `/categories` | Creator categories |
| GET | `/r/{link}` | Track click + redirect |
### Auth
| Method | Path | Description |
|--------|------|-------------|
| POST | `/register` | Create account |
| POST | `/login` | Session login |
| POST | `/logout` | End session |
| GET | `/me` | Current user + profile |
| POST | `/forgot-password` | Request reset email |
| POST | `/password/reset` | Reset with token |
### Authenticated (`auth` middleware)
| Method | Path | Description |
|--------|------|-------------|
| GET/PUT | `/profile` | Read/update profile (incl. theme_id) |
| GET | `/analytics/summary` | Views + clicks totals |
| GET/POST/PUT/DELETE | `/links` | Link CRUD |
| POST | `/links/reorder` | Reorder links |
| GET/POST/PUT/DELETE | `/blocks` | Widget CRUD |
| POST | `/blocks/reorder` | Reorder widgets |
| GET/POST/DELETE | `/media` | File list / upload / delete |
| GET/POST/PUT/DELETE | `/projects` | Portfolio CRUD |
| POST | `/projects/reorder` | Reorder projects |
| POST | `/onboarding/social-links` | Save social usernames |
| POST | `/onboarding/profile` | Avatar/video upload |
| POST | `/onboarding/complete` | Publish profile |
All authenticated routes use **session cookies** — frontend must send `credentials: "include"`.
CSRF: API routes under `api/*` are exempt from CSRF (see `bootstrap/app.php`).
---
## 8. Frontend Routes & Pages
### Marketing & public
| Route | Page | Auth |
|-------|------|------|
| `/` | Landing (Index) | No |
| `/explore` | Creator discovery | No |
| `/login` | Login | No |
| `/signup` | Signup | No |
| `/{slug}` | Public link-in-bio page | No |
### Onboarding (auth required)
| Route | Purpose |
|-------|---------|
| `/onboarding/socials` | Add social accounts |
| `/onboarding/content` | Avatar, video, bio |
| `/onboarding/finish` | Copy link + celebrate |
### Dashboard (auth required)
| Route | Purpose | API wired? |
|-------|---------|------------|
| `/dashboard` | Overview + quick actions | Yes |
| `/dashboard/links` | Link editor | Yes |
| `/dashboard/profile` | Name, bio, socials | Yes |
| `/dashboard/appearance` | Theme picker | Yes |
| `/dashboard/widgets` | Bento blocks | Yes |
| `/dashboard/files` | Images, videos, docs, résumé | Yes |
| `/dashboard/portfolio` | Gallery projects | Yes |
| `/dashboard/analytics` | Views & clicks | Yes |
| `/dashboard/connect-github` | GitHub username | Yes |
| `/dashboard/settings` | Account settings | Partial |
| `/dashboard/skills` | Skills list | Placeholder (local only) |
| `/dashboard/work` | Work history | Placeholder |
| `/dashboard/subscriptions` | Pricing cards | Placeholder |
Legacy redirects:
- `/dashboard/upload-videos` → `/dashboard/files?tab=video`
- `/dashboard/documents` → `/dashboard/files?tab=document`
- `/dashboard/upload-resume` → `/dashboard/files?tab=resume`
- `/dashboard/connect-linkedin` → `/dashboard/profile`
---
## 9. Feature Deep Dives
### 9.1 Authentication
- **Session-based** (not JWT)
- Cookie set on login; sent automatically with `fetch(..., { credentials: "include" })`
- `ProtectedRoute` component redirects unauthenticated users to `/login`
- After logout, `GET /api/me` returns 401 (not 500)
### 9.2 Public profile rendering
Main file: `frontend/src/pages/web/PublicProfile.tsx`
Component tree:
```
PublicProfilePage
├── ProfileHeader (avatar, name, bio, share)
├── [content card]
│ ├── Video (optional)
│ ├── Résumé button (optional)
│ ├── PublicLinkStack (featured + standard links)
│ ├── PublicProjectGrid (portfolio)
│ └── PublicBlockGrid (widgets)
├── PublicSocialBar
└── Footer branding
```
Theme resolution: `frontend/src/lib/publicTheme.ts`
### 9.3 Dashboard navigation
`frontend/src/components/DashboardLayout.tsx` — pill-style header nav.
Only the **active** route expands with a label (uses React Router `NavLink` with `end` on Home so `/dashboard` does not stay active on sub-routes).
### 9.4 File upload flow
1. User selects file in Files page
2. `FormData` POST to `/api/media` with `file` + `kind`
3. Laravel stores file on `public` disk, creates `media` row
4. For `kind=resume`, also updates `profiles.resume_url`
5. Public profile shows "Download résumé" button
Run once after setup:
```powershell
docker compose exec frankenphp php artisan storage:link
```
### 9.5 Link redirect & analytics
`RedirectController` records:
- `link_id`, `ip`, user agent, referrer, timestamp
Then returns HTTP redirect to the link's stored URL.
---
## 10. Local Development Setup
### Prerequisites
- Docker Desktop (running)
- Node.js 18+ (for frontend)
- Git
### Step 1 — Backend
```powershell
cd e:\bento\frankin-app
# Copy env if needed
copy .env.example .env
# Set APP_KEY via: php artisan key:generate (inside container)
# Build & start (FrankenPHP profile — recommended)
.\build-and-run.ps1 -Profile frankenphp
# Migrations + storage + demo data
docker compose exec frankenphp php artisan migrate --force
docker compose exec frankenphp php artisan storage:link
docker compose exec frankenphp php artisan db:seed --class=ThemeSeeder --force
docker compose exec frankenphp php artisan db:seed --class=SmokeDemoSeeder --force
```
Backend API: **http://localhost:8002**
> **Note:** With `-Profile frankenphp`, there is **no `app` container**. Run artisan inside **`frankenphp`**:
> `docker compose exec frankenphp php artisan ...`
### Step 2 — Frontend
```powershell
cd e:\bento\frontend
# Ensure .env exists
# VITE_API_URL=http://localhost:8002
npm install
npm run dev
```
Frontend SPA: **http://localhost:8080**
Vite proxies `/api/*` → `http://localhost:8002` (see `vite.config.ts`).
> Restart Vite after changing `frontend/.env`.
### Step 3 — Verify
| URL | Expected |
|-----|----------|
| http://localhost:8080/ | Landing page |
| http://localhost:8080/smoke817393 | Demo public profile |
| http://localhost:8080/login | Login |
| http://localhost:8002/api/themes | JSON theme list |
Run full smoke tests:
```powershell
cd e:\bento\frankin-app
.\tests\e2e-full.ps1
```
---
## 11. Docker & Services
### Profiles (`build-and-run.ps1`)
| Profile | Services started | Use case |
|---------|------------------|----------|
| `frankenphp` | postgres, redis, frankenphp | **Default** — fast Octane API on :8002 |
| `php-fpm` | postgres, redis, app, web | Traditional Nginx + PHP-FPM on :8001 |
| `all` | Everything | Full stack |
Optional flags: `-WithNode`, `-WithPgAdmin`, `-Down`, `-Logs`, `-BuildOnly`, `-NoBuild`
### Common commands
```powershell
cd e:\bento\frankin-app
docker compose ps
docker compose logs -f frankenphp
docker compose exec frankenphp php artisan migrate --force
docker compose exec frankenphp php artisan db:seed --class=SmokeDemoSeeder --force
docker compose down
```
---
## 12. Environment Variables
### Backend (`frankin-app/.env`)
| Variable | Purpose |
|----------|---------|
| `APP_URL` | Base URL for generated links |
| `APP_KEY` | Laravel encryption key |
| `DB_*` | PostgreSQL connection (host `postgres` in Docker) |
| `REDIS_HOST` | Redis (`redis` in Docker) |
| `CACHE_STORE` | `redis` |
| `SESSION_DRIVER` | `database` |
| `FILESYSTEM_DISK` | `local` (use `public` disk in code for uploads) |
### Frontend (`frontend/.env`)
| Variable | Purpose |
|----------|---------|
| `VITE_API_URL` | API origin — **must be HTTP port 8002**, not HTTPS 8445 |
Example:
```env
VITE_API_URL=http://localhost:8002
```
If API URL is wrong or stale, public profiles may 404 or hit the wrong host. Always restart Vite after changes.
---
## 13. Testing
### E2E smoke script
`frankin-app/tests/e2e-full.ps1` — tests:
- Public profile, themes, creators, slug check
- Register/login/logout flow
- Links, blocks, media, projects CRUD
- Analytics summary
- Frontend proxy through `:8080`
Requires backend on `:8002` and optionally frontend on `:8080`.
### PHPUnit / Pest
Located in `frankin-app/tests/` — auth and feature tests for Laravel.
---
## 14. Demo Account & Seed Data
### Smoke demo user
| Field | Value |
|-------|-------|
| Email | `smoke817393@test.com` |
| Password | `password123` |
| Slug | `smoke817393` |
| Public URL | http://localhost:8080/smoke817393 |
### Re-seed demo content
```powershell
docker compose exec frankenphp php artisan db:seed --class=SmokeDemoSeeder --force
```
Seeds: profile, social links, links (with featured thumbnails), blocks, portfolio projects, résumé URL, Sunset/Neon theme assignment.
### Theme presets
Seeded by `ThemeSeeder`: **Classic**, **Midnight**, **Sunset**, **Minimal**, **Neon**
---
## 15. Key Source Files
### Backend
| File | Purpose |
|------|---------|
| `routes/api.php` | All REST routes |
| `app/Http/Controllers/Api/ProfileController.php` | Public + owner profile |
| `app/Http/Controllers/Api/LinkController.php` | Links CRUD |
| `app/Http/Controllers/Api/BlockController.php` | Widgets CRUD |
| `app/Http/Controllers/Api/MediaController.php` | File uploads |
| `app/Http/Controllers/Api/ProjectController.php` | Portfolio CRUD |
| `app/Http/Controllers/Api/ThemeController.php` | Theme list |
| `app/Http/Controllers/Api/RedirectController.php` | Click tracking redirect |
| `app/Http/Controllers/Api/AnalyticsController.php` | Summary stats |
| `database/seeders/SmokeDemoSeeder.php` | Demo profile data |
| `database/seeders/ThemeSeeder.php` | Theme presets |
### Frontend
| File | Purpose |
|------|---------|
| `src/App.tsx` | All routes |
| `src/lib/apiBase.ts` | API URL helper |
| `src/lib/authService.ts` | Login/logout/me |
| `src/lib/profileService.ts` | Profile + links client |
| `src/lib/blockService.ts` | Widgets client |
| `src/lib/mediaService.ts` | Files upload client |
| `src/lib/projectService.ts` | Gallery client |
| `src/lib/themeService.ts` | Theme save client |
| `src/lib/publicTheme.ts` | Public page theme presets |
| `src/pages/web/PublicProfile.tsx` | Public link page |
| `src/components/DashboardLayout.tsx` | Dashboard shell + nav |
| `vite.config.ts` | Dev server + API proxy |
---
## 16. Implementation Status
### Done (Phases 1–3)
- [x] Session auth (register, login, logout, me)
- [x] Slug-based public profiles
- [x] Links CRUD, reorder, featured thumbnails
- [x] Theme picker + public theme rendering
- [x] Onboarding flow
- [x] Analytics (views, clicks, summary)
- [x] Explore / creators API
- [x] Bento widgets (hero, stats, gallery, testimonials)
- [x] Unified Files upload (images, video, docs, résumé)
- [x] Portfolio gallery projects
- [x] OG meta tags on public profiles
- [x] Docker FrankenPHP stack + e2e tests
### Not yet implemented (Phase 4+)
- [ ] Stripe billing / subscriptions (UI placeholder exists)
- [ ] Custom domains
- [ ] Email verification fully wired in UI
- [ ] Google OAuth
- [ ] Feature gating by plan
- [ ] S3 cloud storage (local disk only today)
- [ ] Skills / Work dashboard pages (placeholders)
See `docs/LINK_IN_BIO_PRODUCT_SPEC.md` for full product roadmap.
---
## 17. Troubleshooting
### `service "app" is not running`
You started with `-Profile frankenphp`. Use:
```powershell
docker compose exec frankenphp php artisan migrate
```
Not `docker compose exec app ...`.
### Public profile 404
1. Check `frontend/.env` has `VITE_API_URL=http://localhost:8002`
2. Restart Vite (`npm run dev`)
3. Confirm profile exists and `is_published = true`
4. Confirm slug matches URL exactly
### Theme saves but page looks the same
1. Hard refresh public page (`Ctrl+Shift+R`)
2. Restart Vite after frontend theme code changes
3. Themes render via `publicTheme.ts` keyed by theme **name** — ensure theme name matches a preset (Classic, Midnight, etc.)
### Upload fails
1. Run `php artisan storage:link` inside `frankenphp` container
2. Check `storage/app/public` is writable
3. Verify file size/type limits in `MediaController`
### CORS / cookie issues
Use Vite dev server (`:8080`) with proxy — do not open frontend from a different origin than API without CORS config. Session cookies require same-site or proxied `/api` path.
### Port conflicts
| Port | Service |
|------|---------|
| 8002 | FrankenPHP HTTP |
| 8080 | Vite frontend |
| 5434 | PostgreSQL |
| 6380 | Redis (host maps to 6379 in container) |
Redis host port 6380 avoids conflict with local Redis on 6379.
---
## 18. Related Documents
| Document | Contents |
|----------|----------|
| [LINK_IN_BIO_PRODUCT_SPEC.md](./LINK_IN_BIO_PRODUCT_SPEC.md) | Product research, UX flows, phased tasks, acceptance criteria |
| [frontend/README.md](../frontend/README.md) | Vite/React template readme (if present) |
---
## Quick Command Cheat Sheet
```powershell
# Start backend
cd e:\bento\frankin-app
.\build-and-run.ps1 -Profile frankenphp
# Migrate + seed
docker compose exec frankenphp php artisan migrate --force
docker compose exec frankenphp php artisan storage:link
docker compose exec frankenphp php artisan db:seed --class=SmokeDemoSeeder --force
# Start frontend
cd e:\bento\frontend
npm run dev
# Run tests
cd e:\bento\frankin-app
.\tests\e2e-full.ps1
# Demo login
# smoke817393@test.com / password123
# http://localhost:8080/smoke817393
```
---
*Mecca Digital — Everything you create. One link in bio.*
Project Gallery
Visual Showcase
Interested in Similar Work?
Let's discuss how we can create something amazing for your business with the same level of engineering excellence.