# Volubit CMS & Admin Content Control System Product Requirements Document

> **Version**: 1.0
> **Date**: August 2026
> **Status**: Draft
> **Stakeholders**: Volubit Team

---

## Table of Contents

1. [Overview](#1-overview)
2. [Design Philosophy](#2-design-philosophy)
3. [Goals & Non-Goals](#3-goals--non-goals)
4. [Content Inventory (Structure)](#4-content-inventory)
5. [Content Schema](#5-content-schema)
5B. [Content Inventory (Existing Hardcoded Content)](#5b-content-inventory-existing-hardcoded-content)
6. [API Design](#6-api-design)
7. [Project Structure](#7-project-structure)
8. [Database Schema](#8-database-schema)
9. [User Flows](#9-user-flows)
10. [Implementation Plan](#10-implementation-plan)
11. [Success Metrics](#11-success-metrics)
12. [Open Decisions](#12-open-decisions)

---

## 1. Overview

### 1.1 Current State

Volubit is a premium crypto education community platform built with:

- **Frontend**: TanStack Start (React Router v7), TypeScript, shadcn/ui, Tailwind CSS v4, Framer Motion
- **Backend**: Static-first frontend with all content hardcoded in TypeScript files
- **Content**: Every piece of copy, image reference, stat, schedule item, and CTA is defined in source files at compile time

The current content architecture means:

- Updating a testimonial requires a code change + CI pipeline + deploy
- Adding an event requires a developer
- Changing a headline requires a PR review cycle
- Content teams cannot self-serve

### 1.2 Target State

Split the project into two decoupled repositories:

| Repository         | Role                                                | Technology                      |
| ------------------ | --------------------------------------------------- | ------------------------------- |
| `volubit-frontend` | Public-facing website, SSR rendering, UI components | TanStack Start + existing stack |
| `volubit-backend`  | Content API, admin panel, media storage, auth       | Nitro + Postgres + Drizzle ORM  |

The frontend fetches all content from the backend API. Content editors use the admin panel — no code, no deploys.

### 1.3 Why Split?

- **Deployment independence**: FE deploys on every code change; BE deploys only on logic changes
- **Team clarity**: FE team and content team work independently
- **Scalability**: FE served from CDN/edge; BE scales with actual traffic
- **Security**: Database is only accessible via BE API; FE only holds public API endpoints
- **CMS**: A proper admin panel enables non-technical content management

---

## 2. Design Philosophy

### Core Principle: Structure is Developer-Defined, Content is Editor-Managed

The key principle of this CMS:

> **Developers define the structure. Editors edit the content.**

This means:

- **What content exists** (sections, items, fields) is decided by developers in code
- **What the content says** (text, images, values) is managed by editors via the admin panel
- Editors **cannot** create new sections, delete sections, or change field structure
- Editors **can** add new items within existing sections (up to a defined cap per section)
- Editors **cannot** reorder items — order is fixed as defined by developer
- Changes save **immediately** — no draft state, no publish action

### Why This Approach?

- **Simplicity**: Eliminates draft/publish workflow, versioning, and rollback complexity
- **Safety**: Editors can't accidentally break page structure or remove content
- **Speed**: Content changes go live instantly with no approval step
- **Maintainability**: Backend API is simpler, fewer edge cases, less to debug

### What Developers Control

- Page structure (which sections exist on each page)
- Section field definitions (what data each block type holds)
- Item count caps (maximum items per section)
- Item ordering (fixed, set at seed time)
- Block types and schema

### What Editors Control

- All text content (headlines, descriptions, CTAs, names, etc.)
- All image references (upload new images, assign to content blocks)
- All numeric/string values (prices, dates, counts, etc.)
- Adding new items within the section cap
- Removing existing items (down to a minimum defined by developer)

---

## 3. Goals & Non-Goals

### 3.1 Goals

1. **Decouple content from code** — All marketing copy, section items, images, and data come from the BE API, not source files
2. **Enable non-technical content management** — Any team member can update content via the admin panel
3. **Immediate saves** — No draft/publish step; changes go live instantly
4. **Server-side rendering performance** — FE pages render with content server-side for fast load and SEO
5. **Media management** — Upload, organize, and reference images without external tools
6. **Audit trail** — Track who changed what and when

### 3.2 Non-Goals

1. **User-generated content** — No public-facing forum, comments, or member-posted content
2. **Multi-tenant CMS** — This is for Volubit only, not a multi-site platform
3. **Draft → Publish workflow** — Changes are immediate, no staging state
4. **Version history & rollback** — Not in scope for v1
5. **Publish scheduler** — Not in scope for v1
6. **A/B testing** — Not in scope for Phase 1
7. **Localization (i18n)** — Content translation not in scope for v1
8. **E-commerce or payment integration** — Checkout and membership billing remain separate
9. **Email automation** — Admin notifications out of scope for v1

---

## 4. Content Inventory

### 4.1 Home Page

| Section Key          | Section Name          | Block Type         | Item Count | Soft Cap | Change Frequency |
| -------------------- | --------------------- | ------------------ | ---------- | -------- | ---------------- |
| `hero`               | Hero                  | `HeroBlock`        | 1          | 1        | Low              |
| `why-volubit`        | Why Volubit           | `CardGridBlock`    | 6          | 8        | Low              |
| `community-showcase` | Community Showcase    | `CustomBlock`      | 1          | 1        | Low              |
| `mentors`            | Mentors / Instructors | `MentorBlock`      | 3          | 10       | Medium           |
| `upcoming-events`    | Upcoming Events       | `EventBlock`       | 3          | 10       | High             |
| `gallery`            | Community Gallery     | `GalleryBlock`     | 11         | 20       | Medium           |
| `research-preview`    | Research Preview      | `ResearchBlock`    | 6          | 10       | High             |
| `latest-articles`    | Latest Articles       | `ArticleBlock`     | 4          | 10       | High             |
| `partnership`        | Partnership           | `PartnerBlock`     | 3 groups   | 5 groups | Low              |
| `testimonials`       | Testimonials          | `TestimonialBlock` | 3          | 10       | Medium           |
| `collab-cta`         | Collaboration CTA     | `CTABlock`         | 1          | 1        | Low              |
| `join-cta`           | Join CTA              | `CTABlock`         | 1          | 1        | Low              |

#### 4.1.1 Hero Block Fields

- `eyebrow`: string (e.g. "186 members online right now")
- `headline`: string (e.g. "Learn crypto with people who actually do it.")
- `subheadline`: string (e.g. "Volubit is a members-only education community...")
- `ctaPrimary`: `{ label: string, href: string }` (e.g. "Become a member" → "/checkout")
- `ctaSecondary`: `{ label: string, href: string }` (e.g. "Explore the community" → "/community")
- `stats`: Array of `{ value: number, suffix: string, label: string }` (3 items)
- `trustedBy`: Array of `string` (4 brand names)

#### 4.1.2 Why Volubit CardGridBlock (soft cap: 8 items)

- Each card: `{ icon: string, title: string, description: string, benefit: string }`
- `icon`: string name of Lucide icon to render

#### 4.1.3 Community Showcase CustomBlock

- `feed`: Array of `{ initials: string, name: string, message: string, timeAgo: string }` (4 items)
- `ticker`: Array of `string` (6 event text items for marquee)
- `voiceRoomLabel`: string
- `voiceRoomParticipants`: number
- `studyGroup`: `{ name: string, week: number, totalWeeks: number }`

#### 4.1.4 Mentors MentorBlock (soft cap: 10 items)

- `{ name: string, role: string, experience: string, specialization: string, courses: string, certifications: string, photo: MediaBlock | null }`

#### 4.1.5 Upcoming Events EventBlock (soft cap: 10 items)

- `{ title: string, speaker: string, seats: number, place: string, date: string, time: string, agenda: string[], image: MediaBlock | null }`

#### 4.1.6 Research Preview ResearchBlock (soft cap: 10 items)

- `{ title: string, tag: string, locked: boolean, preview: string | null }`

#### 4.1.7 Latest Articles ArticleBlock (soft cap: 10 items)

- `{ title: string, category: string, readTime: string, author: string, image: MediaBlock | null }`
- Also: `categories`: Array of `string` (6 category names)

### 4.2 Community Page

| Section Key    | Section Name         | Block Type         | Item Count | Soft Cap | Change Frequency |
| -------------- | -------------------- | ------------------ | ---------- | -------- | ---------------- |
| `hero`         | Hero                 | `HeroBlock`        | 1          | 1        | Low              |
| `stats`        | Community Stats      | `StatsBlock`       | 4          | 6        | Low              |
| `showcase`     | Inside the Community | `CustomBlock`      | 1          | 1        | Low              |
| `activities`   | Activities           | `CardGridBlock`    | 4          | 8        | Low              |
| `schedule`     | Weekly Schedule      | `ScheduleBlock`    | 7          | 7 (fixed) | Medium           |
| `instructors`  | Mentors              | `MentorBlock`      | 3          | 10       | Medium           |
| `gallery`      | Gallery              | `GalleryBlock`     | 11         | 20       | Medium           |
| `testimonials` | Testimonials         | `TestimonialBlock` | 3          | 10       | Medium           |
| `faq`          | FAQ                  | `FAQBlock`         | 3          | 10       | Low              |
| `join-cta`     | Join CTA             | `CTABlock`         | 1          | 1        | Low              |

### 4.3 About Page

| Section Key   | Section Name | Block Type      | Item Count | Soft Cap | Change Frequency |
| ------------- | ------------ | --------------- | ---------- | -------- | ---------------- |
| `hero`        | Hero         | `HeroBlock`     | 1          | 1        | Low              |
| `vision`      | Vision       | `RichTextBlock` | 1          | 1        | Very Low         |
| `mission`     | Mission      | `RichTextBlock` | 1          | 1        | Very Low         |
| `timeline`    | Timeline     | `ScheduleBlock` | 5          | 5 (fixed) | Very Low         |
| `instructors` | Mentors      | `MentorBlock`   | 3          | 10       | Medium           |
| `culture`     | Culture      | `CardGridBlock` | 4          | 8        | Very Low         |
| `gallery`     | Gallery      | `GalleryBlock`  | 11         | 20       | Medium           |
| `join-cta`    | Join CTA     | `CTABlock`      | 1          | 1        | Low              |

### 4.4 Events Page

| Section Key       | Section Name    | Block Type         | Item Count | Soft Cap | Change Frequency |
| ----------------- | --------------- | ------------------ | ---------- | -------- | ---------------- |
| `hero`            | Hero            | `HeroBlock`        | 1          | 1        | Low              |
| `featured-event`  | Featured Event  | `EventBlock`       | 1          | 1        | High             |
| `upcoming-events` | Upcoming Events | `EventBlock`       | 3          | 10       | High             |
| `calendar`        | Calendar        | `ScheduleBlock`    | 5          | 5 (fixed) | High             |
| `instructors`     | Mentors         | `MentorBlock`      | 3          | 10       | Medium           |
| `gallery`         | Gallery         | `GalleryBlock`     | 11         | 20       | Medium           |
| `testimonials`    | Testimonials    | `TestimonialBlock` | 3          | 10       | Medium           |
| `join-cta`        | Join CTA        | `CTABlock`         | 1          | 1        | Low              |

### 4.5 Articles Page

| Section Key        | Section Name     | Block Type      | Item Count | Soft Cap | Change Frequency |
| ------------------ | ---------------- | --------------- | ---------- | -------- | ---------------- |
| `hero`             | Hero             | `HeroBlock`     | 1          | 1        | Low              |
| `latest-articles`  | Latest Articles  | `ArticleBlock`  | 4          | 10       | High             |
| `categories`       | Categories       | `Array<string>` | 6          | 10       | Low              |
| `popular`          | Popular Articles | `Array<string>` | 4          | 10       | Medium           |
| `newsletter`       | Newsletter       | `RichTextBlock` | 1          | 1        | Low              |
| `related`          | Related Articles | `ArticleBlock`  | 3          | 10       | High             |
| `join-cta`         | Join CTA         | `CTABlock`      | 1          | 1        | Low              |

### 4.6 FAQ Page

| Section Key  | Section Name | Block Type       | Item Count | Soft Cap | Change Frequency |
| ------------ | ------------ | ---------------- | ---------- | -------- | ---------------- |
| `hero`       | Hero         | `HeroBlock`      | 1          | 1        | Low              |
| `faq-groups` | FAQ Groups   | `FAQGroupBlock`  | 4          | 6        | Low              |

**FAQGroupBlock**: Array of `{ title: string, items: Array<[question: string, answer: string]> }` (max 5 items per group)
**Soft cap** on FAQ groups: 6

### 4.7 Membership / Pricing Page

| Section Key          | Section Name      | Block Type            | Item Count | Soft Cap | Change Frequency |
| -------------------- | ----------------- | --------------------- | ---------- | -------- | ---------------- |
| `hero`               | Hero              | `HeroBlock`           | 1          | 1        | Low              |
| `plans`              | Plans             | `PricingBlock.plans`  | 3          | 5        | Low              |
| `comparison-matrix`  | Comparison Matrix | `PricingBlock.matrix` | 9          | 9 (fixed) | Low              |
| `join-cta`           | Join CTA          | `CTABlock`            | 1          | 1        | Low              |

**PricingBlock.plans**: Array of `{ name: string, price: string, period: string, note: string, featured: boolean }`
**PricingBlock.matrix**: Array of `[featureName: string, monthly: boolean, quarterly: boolean, yearly: boolean]` (fixed structure)

### 4.8 Partnership Page

| Section Key    | Section Name | Block Type      | Item Count | Soft Cap | Change Frequency |
| -------------- | ------------ | --------------- | ---------- | -------- | ---------------- |
| `hero`         | Hero         | `HeroBlock`     | 1          | 1        | Low              |
| `partners`     | Partners     | `PartnerBlock`  | 3 groups   | 5 groups | Low              |
| `benefits`     | Benefits     | `CardGridBlock` | 4          | 8        | Low              |
| `process`      | Process      | `CardGridBlock` | 4          | 8        | Low              |
| `inquiry-form` | Inquiry Form | `RichTextBlock` | 1          | 1        | Very Low         |

### 4.9 Contact Page

| Section Key | Section Name     | Block Type      | Item Count | Soft Cap | Change Frequency |
| ----------- | ---------------- | --------------- | ---------- | -------- | ---------------- |
| `hero`      | Hero             | `HeroBlock`     | 1          | 1        | Low              |
| `channels`  | Contact Channels | `ContactBlock`  | 4          | 6        | Low              |
| `office`    | Office Info      | `RichTextBlock` | 1          | 1        | Very Low         |
| `form`      | Contact Form     | `RichTextBlock` | 1          | 1        | Very Low         |
| `faq`       | Quick FAQ        | `FAQBlock`      | 3          | 10       | Low              |

**ContactBlock**: Array of `{ icon: string, channelName: string, value: string, note: string }`

### 4.10 Global / Site Settings

| Field Key         | Field Type                                                   | Description              |
| ----------------- | ------------------------------------------------------------ | ------------------------ |
| `siteName`        | string                                                       | Volubit                  |
| `tagline`         | string                                                       | Primary tagline          |
| `logo`            | MediaBlock                                                   | Primary logo image       |
| `favicon`         | MediaBlock                                                   | Favicon image            |
| `socialLinks`     | `{ twitter?: string, instagram?: string, discord?: string }` | Social URLs              |
| `defaultOgImage`  | MediaBlock                                                   | Default Open Graph image |
| `footerCopyright` | string                                                       | Copyright text           |
| `announcementBar` | `{ enabled: boolean, text: string, href?: string }`         | Top banner               |

---

## 5. Content Schema

### 5.1 Block Type Definitions

```typescript
// HeroBlock — one per page hero
interface HeroBlock {
  eyebrow: string;
  headline: string;
  description: string;
  ctaPrimary: { label: string; href: string };
  ctaSecondary: { label: string; href: string };
  stats?: Array<{ value: number; suffix: string; label: string }>;
  trustedBy?: string[];
}

// StatsBlock — stat cards row
interface StatsBlock {
  items: Array<{ value: number; suffix: string; label: string }>;
}

// CardGridBlock — generic card grid (features, benefits, culture)
interface CardGridBlock {
  eyebrow?: string;
  title?: string;
  description?: string;
  align?: "left" | "center";
  items: Array<{
    title: string;
    description: string;
    badge?: string;
    icon?: string; // Lucide icon name
    image?: MediaBlock;
  }>;
}

// ScheduleBlock — day-activity rows (order is fixed)
interface ScheduleBlock {
  items: Array<[day: string, activity: string]>;
}

// FAQBlock — Q&A pairs
interface FAQBlock {
  items: Array<[question: string, answer: string]>;
}

// FAQGroupBlock — grouped Q&A
interface FAQGroupBlock {
  groups: Array<{
    title: string;
    items: Array<[question: string, answer: string]>;
  }>;
}

// TestimonialBlock — member quotes
interface TestimonialBlock {
  items: Array<{
    quote: string;
    name: string;
    occupation: string;
    avatar?: string;
  }>;
}

// EventBlock — event cards
interface EventBlock {
  items: Array<{
    title: string;
    speaker: string;
    seats: number;
    place: string;
    date: string;
    time: string;
    agenda: string[];
    image?: MediaBlock;
  }>;
}

// ArticleBlock — article cards
interface ArticleBlock {
  categories?: string[];
  items: Array<{
    title: string;
    category: string;
    readTime: string;
    author: string;
    image?: MediaBlock;
  }>;
}

// PartnerBlock — partner listing
interface PartnerBlock {
  groups: Array<{
    groupLabel: string;
    items: string[];
  }>;
}

// MentorBlock — instructor cards
interface MentorBlock {
  items: Array<{
    name: string;
    role: string;
    experience: string;
    specialization: string;
    courses: string;
    certifications: string;
    photo?: MediaBlock;
  }>;
}

// ResearchBlock — research preview cards
interface ResearchBlock {
  items: Array<{
    title: string;
    tag: string;
    locked: boolean;
    previewText?: string;
    chartData?: number[];
  }>;
}

// GalleryBlock — image grid
interface GalleryBlock {
  items: Array<{
    src: string;
    alt: string;
    width: number;
    height: number;
    layoutHint?: "portrait" | "landscape" | "square";
  }>;
}

// PricingBlock — plans + comparison
interface PricingBlock {
  plans: Array<{
    name: string;
    price: string;
    period: string;
    note: string;
    featured: boolean;
  }>;
  matrix: Array<[string, boolean, boolean, boolean]>; // [feature, monthly, quarterly, yearly]
}

// CTABlock — call-to-action block
interface CTABlock {
  title: string;
  subtitle?: string;
  ctaLabel: string;
  ctaHref: string;
  ctaSecondaryLabel?: string;
  ctaSecondaryHref?: string;
  chips?: string[];
}

// RichTextBlock — free-form HTML/Markdown
interface RichTextBlock {
  content: string; // HTML or Markdown
}

// ContactBlock — contact channel cards
interface ContactBlock {
  items: Array<{
    icon: string; // Lucide icon name
    channelName: string;
    value: string;
    note: string;
  }>;
}

// SiteSettingsBlock — global settings (singleton)
interface SiteSettingsBlock {
  siteName: string;
  tagline: string;
  logo?: MediaBlock;
  favicon?: MediaBlock;
  socialLinks: {
    twitter?: string;
    instagram?: string;
    discord?: string;
  };
  defaultOgImage?: MediaBlock;
  footerCopyright: string;
  announcementBar: {
    enabled: boolean;
    text: string;
    href?: string;
  };
}

// MediaBlock — image reference
interface MediaBlock {
  url: string;
  alt: string;
  width: number;
  height: number;
  mimeType: string;
  size: number; // bytes
}
```

### 5.2 Content Block Database Row

Every content block in the database uses this shape:

```typescript
interface ContentBlockRow {
  id: string; // UUID v4
  page: PageKey; // "home" | "community" | "about" | "events" | "articles" | "faq" | "membership" | "partnership" | "contact"
  section: string; // e.g. "hero", "upcoming-events", "testimonials"
  blockType: BlockType; // e.g. "HeroBlock", "CardGridBlock"
  locale: "en" | "id"; // future i18n; default "en"
  data: any; // typed as the appropriate block type
  createdAt: string;
  updatedAt: string;
  updatedBy: string; // admin ID
}
```

---

## 5B. Content Inventory (Existing Hardcoded Content)

This section documents every piece of hardcoded content currently in the codebase — the content that will be migrated into the CMS. Each entry shows the current value as it appears in the source files.

> **Note:** Image paths reference local assets (`@/assets/...`). These will be migrated to the media library. Editors can replace these with new uploaded images or keep the same references.

---

### 5B.1 Home Page (`/`)

#### Hero Section (`hero`)

| Field | Current Value | Type |
|-------|--------------|------|
| `eyebrow` | "186 members online right now" | text |
| `headline` | "Learn crypto with people who actually do it." | text |
| `description` | "Volubit is a members-only education community. Professional research, weekly live classes, structured curriculum and mentors who answer in the same room you learn in." | text |
| `ctaPrimary.label` | "Become a member" | text |
| `ctaPrimary.href` | "/checkout" | text |
| `ctaSecondary.label` | "Explore the community" | text |
| `ctaSecondary.href` | "/community" | text |
| `stats[0]` | `{ value: 5200, suffix: "+", label: "Members" }` | stat |
| `stats[1]` | `{ value: 180, suffix: "+", label: "Research notes" }` | stat |
| `stats[2]` | `{ value: 96, suffix: "%", label: "Renew yearly" }` | stat |
| Floating card 1 (Discord) | `#market-talk, "ETH funding flipped negative again."`, `"Chart in the research note, page 4."` | text |
| Floating card 2 (Portfolio) | `"Members' allocation study, Q3"`, `+4.2%` | text |
| Floating card 3 (Research) | `"Macro liquidity and the Bitcoin cycle: what changed this week"`, `"18 pages · published today"` | text |
| Floating card 4 (Voice) | `"Voice · Weekly Class"`, 5 avatar initials | text |
| Image | `@/assets/hero-image` (referenced via community images) | image |

#### Why Volubit Section (`why-volubit`) — CardGridBlock, 6 items

| # | Icon | Title | Description | Benefit |
|---|------|-------|-------------|---------|
| 1 | `LineChart` | "Professional research" | "Weekly market reports written by analysts, not influencers." | "Macro, on-chain and asset notes every week" |
| 2 | `GraduationCap` | "Professional mentors" | "Practitioners with fund, desk and protocol experience." | "Office hours and direct replies" |
| 3 | `BookOpen` | "Weekly class" | "Live sessions with recordings, slides and homework." | "48 sessions per year" |
| 4 | `MessageSquare` | "Discord community" | "Study groups, voice rooms and daily discussion." | "Always-on rooms across timezones" |
| 5 | `ShieldCheck` | "Portfolio discussion" | "Structured reviews focused on risk, not calls." | "Monthly portfolio clinic" |
| 6 | `Handshake` | "Networking" | "Offline meetups, hackathons and hiring circles." | "Events in 6 cities" |

#### Community Showcase (`community-showcase`) — CustomBlock

**Feed (4 items):**
| Initials | Name | Message | Time ago |
|----------|------|---------|----------|
| AR | Andra | "Funding rates reset. Notes in #research." | 2m |
| MJ | Maya | "Joined voice room On-chain 101 starting." | 6m |
| KV | Kevin | "Portfolio clinic recording is up." | 12m |
| TS | Tasya | "Certificate arrived from last workshop!" | 22m |

**Voice Room:**
| Field | Value |
|-------|-------|
| `voiceRoomLabel` | "On-chain Analysis Live" |
| `voiceRoomParticipants` | 6 (avatar initials shown: A, B, C, D, E, F) |
| Pinned message | "📌 Weekly class starts 20:00 WIB in the Main Stage voice room." |

**Mentor Reply:**
| Field | Value |
|-------|-------|
| Quote | "Position sizing before conviction. Write the invalidation level first then decide the size." |
| Attribution | "Rizky · Head of Research" |

**Study Group:**
| Field | Value |
|-------|-------|
| Name | "Technical Analysis Cohort 12" |
| Week | 6 of 8 |

**Ticker (6 items):**
1. "Weekly class · Thursday 20:00"
2. "New research · ETF flows"
3. "Voice room · Macro Q&A"
4. "Meetup · Jakarta, 14 Sep"
5. "Workshop · Risk Management"
6. "Study group · DeFi Cohort 4"

#### Instructors (`mentors`) — MentorBlock, 3 items

| # | Name | Role | Experience | Specialization | Courses | Certifications |
|---|------|------|-----------|---------------|---------|---------------|
| 1 | "Rizky Pratama" | "Head of Research" | "9 yrs · digital asset research" | "Macro & on-chain" | "Macro, On-chain Analysis" | "CFA Level II · CMT I" |
| 2 | "Alina Chandra" | "Lead Mentor" | "7 yrs · portfolio management" | "Risk & allocation" | "Risk Management, Portfolio" | "FRM · CFP" |
| 3 | "Daniel Wijaya" | "Protocol Analyst" | "6 yrs · DeFi engineering" | "DeFi & Web3" | "DeFi, Web3, AI" | "MSc Computer Science" |
| Photo | `@/assets/mentor-1.jpg` | — | — | — | — | (same photo used for all 3 in UI) |

#### Upcoming Events (`upcoming-events`) — EventBlock, 3 items

| # | Title | Speaker | Seats | Place | Date | Agenda |
|---|-------|---------|-------|-------|------|--------|
| 1 | "Macro Outlook Q4 · Live Webinar" | "Rizky Pratama" | 42 | "Online · Zoom" | "12 Sep, 20:00 WIB" | `["Liquidity cycle", "Rates & risk assets", "Positioning Q&A"]` |
| 2 | "On-chain Workshop · Jakarta" | "Daniel Wijaya" | 12 | "Kuningan, Jakarta" | "21 Sep, 13:00 WIB" | `["Dune basics", "Flow metrics", "Build a dashboard"]` |
| 3 | "Portfolio Clinic · Members Only" | "Alina Chandra" | 25 | "Discord Main Stage" | "28 Sep, 19:30 WIB" | `["Allocation review", "Risk scoring", "Rebalancing"]` |
| Images | `@/assets/community-1.jpg`, `@/assets/community-3.jpg` | — | — | — | — | — |

#### Community Gallery (`gallery`) — GalleryBlock, 11 items

| # | Source | Alt text | Layout hint | Dimensions |
|---|--------|---------|------------|------------|
| 1 | `@/assets/community-1.jpg` | "Volubit seminar in Jakarta" | h-72 | 1200×800 |
| 2 | `@/assets/community-2.jpg` | "Mentoring session over coffee" | h-96 | 900×1100 |
| 3 | `@/assets/community-3.jpg` | "Workshop audience" | h-64 | 1000×700 |
| 4 | `@/assets/community-2.jpg` | "Members discussing a portfolio" | h-80 | 900×1100 |
| 5 | `@/assets/community-3.jpg` | "Community meetup" | h-64 | 1000×700 |
| 6 | `@/assets/community-1.jpg` | "Volubit seminar in Jakarta" | h-72 | 1200×800 |
| 7 | `@/assets/community-2.jpg` | "Mentoring session over coffee" | h-96 | 900×1100 |
| 8 | `@/assets/community-3.jpg` | "Workshop audience" | h-64 | 1000×700 |
| 9 | `@/assets/community-2.jpg` | "Members discussing a portfolio" | h-80 | 900×1100 |
| 10 | `@/assets/community-3.jpg` | "Community meetup" | h-64 | 1000×700 |
| 11 | `@/assets/community-1.jpg` | "Trading class in session" | h-80 | 1200×800 |

#### Research Preview (`research-preview`) — ResearchBlock, 6 items

| # | Title | Tag | Locked | Preview Text |
|---|-------|-----|--------|-------------|
| 1 | "Macro Liquidity Monitor" | "Macro" | false | (shows chart + "Weekly update covering flows...") |
| 2 | "Bitcoin Cycle Outlook" | "Bitcoin" | true | — |
| 3 | "Ethereum Supply & Staking" | "Ethereum" | true | — |
| 4 | "Altcoin Rotation Study" | "Altcoins" | true | — |
| 5 | "On-chain Flow Weekly" | "On-chain" | false | (shows chart + "Weekly update...") |
| 6 | "ETF Flows Tracker" | "ETF" | true | — |

#### Latest Articles (`latest-articles`) — ArticleBlock, 4 items + 6 categories

**Categories:** `["All", "Macro", "On-chain", "Education", "DeFi", "Research"]`

**Articles:**
| # | Title | Category | Read time | Author |
|---|-------|---------|-----------|--------|
| 1 | "Why liquidity leads price, and how to track it" | "Macro" | "8 min" | "Rizky" |
| 2 | "A beginner's map of on-chain data" | "On-chain" | "6 min" | "Daniel" |
| 3 | "Risk first: sizing before conviction" | "Education" | "5 min" | "Alina" |
| 4 | "DeFi yields without the fairy tales" | "DeFi" | "9 min" | "Daniel" |

**Featured article:**
| Field | Value |
|-------|-------|
| Title | "The four-year cycle is a story. Here is what the data says instead." |
| Author | "Rizky Pratama" |
| Read time | "12 min read" |
| Image | `@/assets/community-2.jpg` |

#### Partnership Section (`partnership`) — PartnerBlock, 3 groups

| Group | Items |
|-------|-------|
| "Strategic" | `["Nusantara Capital", "Ledgerworks", "Blockchain ID", "Orbit Chain"]` |
| "Corporate" | `["Arta Finance", "Sigma Labs"]` |
| "Community" | `["Dev Circle", "Campus Web3", "Trader Guild", "Node Runners", "Meta Builders"]` |

#### Testimonials (`testimonials`) — TestimonialBlock, 3 items

| # | Quote | Name | Occupation |
|---|-------|------|-----------|
| 1 | "I stopped trading on vibes. The risk framework alone was worth the year." | "Bagas P." | "Product Manager" |
| 2 | "The mentors reply. That sounds small until you've paid for a course where nobody does." | "Ayu R." | "Accountant" |
| 3 | "The weekly research reads like a desk note, not a Telegram signal." | "Fandi S." | "Software Engineer" |

#### Collaboration CTA (`collab-cta`) — CTABlock

| Field | Value |
|-------|-------|
| Title | "Build something with an audience that actually shows up." |
| Description | "We work with corporates, universities, media and communities on education programs, research and events." |
| CTA Label | "Partner with Volubit" |
| CTA Href | "/partnership" |
| CTA Secondary Label | "Talk to us" |
| CTA Secondary Href | "/contact" |
| Chips | `["Corporate Partner", "University Partner", "Media Partner", "Community Partner", "Sponsor"]` |

#### Join CTA (`join-cta`) — CTABlock (shared across pages)

| Field | Value |
|-------|-------|
| Title | "Join 5,200+ people learning crypto properly." (varies per page) |
| Subtitle | "One membership. Research, classes, mentors and the community." |
| CTA Label | "Become a member" |
| CTA Href | "/checkout" |
| CTA Secondary Label | "See pricing" |
| CTA Secondary Href | "/membership/pricing" |

---

### 5B.2 Community Page (`/community`)

#### Page Hero (`hero`)

| Field | Current Value |
|-------|--------------|
| `eyebrow` | "Community" |
| `title` | "The room where the learning actually happens." |
| `description` | "Volubit is not a course you watch alone. It is a room you walk into every day." |

#### Stats (`stats`) — StatsBlock, 4 items

| Value | Suffix | Label |
|-------|--------|-------|
| 5200 | "+" | "Members" |
| 42 | "" | "Study groups" |
| 1420 | "" | "Messages today" |
| 18 | "" | "Cities" |

#### Activities (`activities`) — CardGridBlock, 4 items

| Title | Description |
|-------|-------------|
| "Voice rooms" | "Daily open rooms for questions and chart walkthroughs." |
| "Study groups" | "Cohort-based groups with a mentor and weekly homework." |
| "Community feed" | "Announcements, research drops and member wins." |
| "Offline meetups" | "Coffee sessions, workshops and hackathons." |

#### Weekly Schedule (`schedule`) — ScheduleBlock, 7 items (fixed order)

| Day | Activity |
|-----|---------|
| Monday | "Research drop + weekly outlook" |
| Tuesday | "Beginner study group" |
| Wednesday | "On-chain lab" |
| Thursday | "Weekly live class" |
| Friday | "Mentor office hours" |
| Saturday | "Portfolio clinic" |
| Sunday | "Open voice room" |

#### FAQ (`faq`) — FAQBlock, 3 items

| Question | Answer |
|----------|--------|
| "Is the Discord active?" | "Around 1,400 messages a day across 20+ rooms." |
| "Are meetups included?" | "Yearly members get free entry to offline workshops." |
| "Can beginners keep up?" | "Yes study groups are cohort-based by level." |

> Note: Community page reuses `Instructors`, `CommunityShowcase`, `CommunityGallery`, `Testimonials` sections — see Home Page entries.

---

### 5B.3 About Page (`/about`)

#### Page Hero (`hero`)

| Field | Current Value |
|-------|--------------|
| `eyebrow` | "About" |
| `title` | "Crypto education that respects your intelligence." |
| `description` | "We built the community we wished existed when we started: rigorous, honest and genuinely social." |

#### Vision (`vision`) — RichTextBlock

> "A generation of investors who understand risk before they understand returns."

#### Mission (`mission`) — RichTextBlock

> "Deliver professional-grade research and structured teaching inside a community that keeps people accountable."

#### Timeline (`timeline`) — ScheduleBlock, 5 items (fixed order)

| Year | Event |
|------|-------|
| 2021 | "A Discord server with 40 friends and one weekly call." |
| 2022 | "First paid cohort. The research note becomes weekly." |
| 2023 | "Offline workshops in three cities. 1,200 members." |
| 2024 | "Full curriculum launch and the mentor program." |
| 2026 | "5,200+ members across 18 cities." |

#### Culture (`culture`) — CardGridBlock, 4 items

| Title | Description |
|-------|-------------|
| "Honesty over hype" | "We publish reasoning, not price targets." |
| "Teach the process" | "A framework outlives any single trade." |
| "Show up" | "Community is a habit, not a landing page." |
| "Respect risk" | "Every lesson starts with what can go wrong." |

> Note: About page reuses `Instructors`, `CommunityGallery`, `JoinCTA` — see Home Page entries.

---

### 5B.4 Events Page (`/events`)

#### Page Hero (`hero`)

| Field | Current Value |
|-------|--------------|
| `eyebrow` | "Events" |
| `title` | "Learning that happens in a room, together." |
| `description` | "Webinars every week, workshops every month and offline meetups across the country." |

#### Featured Event (`featured-event`) — EventBlock, 1 item

| Field | Value |
|-------|-------|
| Title | "Macro Outlook Q4 Live Webinar" |
| Description | "A two-hour session on liquidity, rates and how they shape crypto risk appetite going into the final quarter." |
| Speaker | "Rizky Pratama" |
| Date | "12 Sep" |
| Time | "20:00 WIB" |
| Seats | "42 left" |
| Image | `@/assets/community-1.jpg` |

#### Upcoming Events (`upcoming-events`) — EventBlock, 3 items

> Same 3 events as Home Page `upcoming-events` — see Home Page entry.

#### Calendar (`calendar`) — ScheduleBlock, 5 items (fixed order)

| Date | Event | Location |
|------|-------|----------|
| "12 Sep" | "Macro Outlook Q4" | "Online" |
| "21 Sep" | "On-chain Workshop" | "Jakarta" |
| "28 Sep" | "Portfolio Clinic" | "Discord" |
| "05 Oct" | "Risk Management Bootcamp" | "Bandung" |
| "19 Oct" | "Community Meetup" | "Surabaya" |

> Note: Events page reuses `Instructors`, `CommunityGallery`, `Testimonials`, `JoinCTA` — see Home Page entries.

---

### 5B.5 Articles Page (`/articles`)

#### Page Hero (`hero`)

| Field | Current Value |
|-------|--------------|
| `eyebrow` | "Articles" |
| `title` | "Read the thinking behind the research." |

> Note: Articles page currently shows placeholder/empty sections. The `LatestArticles` and `ArticleBlock` components on the Home page show the actual article content — see Home Page entries for `latest-articles`.

---

### 5B.6 FAQ Page (`/faq`)

#### Page Hero (`hero`)

| Field | Current Value |
|-------|--------------|
| `eyebrow` | "FAQ" |
| `title` | "Everything people ask before joining." |
| `description` | "If something is still unclear, message us we answer every question." |

#### FAQ Groups (`faq-groups`) — FAQGroupBlock, 4 groups × 3 items

**Group 1 — Membership:**
| Question | Answer |
|----------|--------|
| "What is included?" | "Courses, weekly research, live classes, Discord and events." |
| "Do I need experience?" | "No. The first track starts from zero." |
| "Can I cancel?" | "Monthly plans cancel any time. Longer plans run to term." |

**Group 2 — Research:**
| Question | Answer |
|----------|--------|
| "Is this financial advice?" | "No. Everything published is educational." |
| "How often is it published?" | "A main report weekly, plus notes through the week." |
| "Can I share it?" | "Research is for members only." |

**Group 3 — Community & events:**
| Question | Answer |
|----------|--------|
| "How do I get into Discord?" | "An invite is emailed instantly after checkout." |
| "Are offline events free?" | "Yearly members get free entry to workshops." |
| "Are sessions recorded?" | "Yes, everything is archived for members." |

**Group 4 — Payment:**
| Question | Answer |
|----------|--------|
| "Which methods work?" | "QRIS, virtual account, credit card and e-wallets." |
| "Do you offer refunds?" | "Yes, within 7 days if you haven't downloaded research." |
| "Do you invoice companies?" | "Yes contact us for corporate billing." |

---

### 5B.7 Membership / Pricing Page (`/membership/pricing`)

#### Page Hero (`hero`)

| Field | Current Value |
|-------|--------------|
| `eyebrow` | "Pricing" |
| `title` | "Priced like a course. Built like a community." |
| `description` | "Every plan includes the community, the classes and the research. Longer plans unlock deeper mentorship." |

#### Plans (`plans`) — PricingBlock.plans, 3 items

| Name | Price | Period | Note | Featured |
|------|-------|--------|------|----------|
| "Monthly" | "Rp 349k" | "/month" | "Cancel any time" | false |
| "Quarterly" | "Rp 899k" | "/3 months" | "Save 14%" | false |
| "Yearly" | "Rp 2.9jt" | "/year" | "Save 31% · most popular" | true |

#### Comparison Matrix (`comparison-matrix`) — PricingBlock.matrix, 9 rows (fixed)

| Feature | Monthly | Quarterly | Yearly |
|---------|---------|-----------|--------|
| "Discord community" | ✓ | ✓ | ✓ |
| "Weekly live class" | ✓ | ✓ | ✓ |
| "Course library" | ✓ | ✓ | ✓ |
| "Weekly research" | ✓ | ✓ | ✓ |
| "Research archive" | ✗ | ✓ | ✓ |
| "Portfolio clinic" | ✗ | ✓ | ✓ |
| "1:1 mentor session" | ✗ | ✗ | ✓ |
| "Offline workshops" | ✗ | ✗ | ✓ |
| "Certificate program" | ✗ | ✗ | ✓ |

---

### 5B.8 Partnership Page (`/partnership`)

#### Page Hero (`hero`)

| Field | Current Value |
|-------|--------------|
| `eyebrow` | "Partnership" |
| `title` | "Reach people who actually show up." |
| `description` | "We collaborate with companies, universities, media and communities on crypto education." |

#### Partner Listing (`partners`) — PartnerBlock, 3 groups

> Same as Home Page `partnership` section — see Home Page entry.

#### Benefits (`benefits`) — CardGridBlock, 4 items

| Title | Description |
|-------|-------------|
| "Engaged audience" | "5,200+ members who attend, not just subscribe." |
| "Education-first" | "Programs built by analysts, not marketers." |
| "Multi-channel" | "Discord, live classes, research and offline events." |
| "Transparent reporting" | "Attendance, engagement and feedback data." |

#### Process (`process`) — CardGridBlock, 4 items (fixed order)

| # | Title | Description |
|---|-------|-------------|
| 01 | "Introduction" | "A 30-minute call to understand your goals." |
| 02 | "Proposal" | "We map a program, audience and timeline." |
| 03 | "Pilot" | "One event or research collaboration to test fit." |
| 04 | "Ongoing" | "A yearly plan with shared reporting." |

#### Inquiry Form (`inquiry-form`) — RichTextBlock

Form labels: "Company", "Contact name", "Email", "Phone", "What do you have in mind?"
Submit button: "Send inquiry"
CTA: Partnership inquiry heading

---

### 5B.9 Contact Page (`/contact`)

#### Page Hero (`hero`)

| Field | Current Value |
|-------|--------------|
| `eyebrow` | "Contact" |
| `title` | "Talk to a human." |
| `description` | "Questions about membership, partnership or events we answer every one." |

#### Contact Channels (`channels`) — ContactBlock, 4 items

| Channel | Icon | Value | Note |
|---------|------|-------|------|
| "WhatsApp" | `MessageCircle` | "+62 811 0000 111" | "Fastest response" |
| "Email" | `Mail` | "hello@volubit.io" | "Within 1 business day" |
| "Office" | `MapPin` | "Kuningan, Jakarta Selatan" | "By appointment" |
| "Business hours" | `Clock` | "Mon–Fri, 09:00–18:00 WIB" | "Discord is 24/7" |

#### Office Info (`office`) — RichTextBlock

| Field | Value |
|-------|-------|
| Label | "Volubit HQ" |
| Address | "Jl. Mars Dirgahayu No.3B Bandung, Indonesia" |

#### Contact Form (`form`) — RichTextBlock

Form labels: "Name", "Email", "Subject", "WhatsApp", "Message"
Submit button: "Send message"
CTA: "Send us a message" heading

#### Quick FAQ (`faq`) — FAQBlock, 3 items

| Question | Answer |
|----------|--------|
| "How fast do you reply?" | "WhatsApp within a few hours on business days." |
| "Can I visit the office?" | "Yes, by appointment message us first." |
| "Where do members get support?" | "Inside Discord, in the #help room." |

---

### 5B.10 Site Settings (Global)

#### Navigation (`nav`)

| Item | Label | To |
|------|-------|----|
| 1 | "Home" | "/" |
| 2 | "Membership" | "/membership" |
| — | "Membership Detail" | "/membership" |
| — | "Pricing" | "/membership/pricing" |
| — | "Checkout" | "/checkout" |
| 3 | "Events" | "/events" |
| — | "Upcoming" | "/events" |
| — | "Past Events" | "/events/past" |
| 4 | "Articles" | "/articles" |

> Note: Community, Partnership, About, Contact are currently commented out in nav.

#### Footer Columns (`footer`)

**Column 1 — Platform:**
| Label | To |
|-------|----|
| "Membership" | "/membership" |
| "Pricing" | "/membership/pricing" |
| "Community" | "/community" |
| "Events" | "/events" |

**Column 2 — Resources:**
| Label | To |
|-------|----|
| "Articles" | "/articles" |
| "FAQ" | "/faq" |
| "Partnership" | "/partnership" |
| "About" | "/about" |

**Column 3 — Contact:**
| Label | To |
|-------|----|
| "Contact us" | "/contact" |
| "Checkout" | "/checkout" |

**Footer Description:**
> "A premium crypto education community. Research, mentorship and a room full of people learning in public."

**Social Links (footer):** `"Discord"`, `"Telegram"`, `"X"`, `"LinkedIn"`, `"YouTube"`

**Copyright:**
> "© {year} Volubit. Educational content only, not financial advice."

**Legal links:** `"Privacy"`, `"Terms"`, `"Disclaimer"`

#### Logo & Branding

| Field | Value |
|-------|-------|
| Logo image | `@/assets/logo.png` |
| Logo alt | "Volubit Logo" |
| Site name | "Volubit" |
| Footer brand mark | "V" (in a primary-colored box) |

#### Announcement Bar

| Field | Value |
|-------|-------|
| `enabled` | `false` (currently not used) |

---

## 6. API Design

### 6.1 Public Endpoints (No Auth)

| Method | Path                          | Description                     |
| ------ | ----------------------------- | ------------------------------- |
| `GET`  | `/api/content/:page`          | All blocks for a page           |
| `GET`  | `/api/content/:page/:section` | Single section                  |
| `GET`  | `/api/site-settings`           | Public site settings            |

**Response Envelope**:

```typescript
{
  success: true,
  data: ContentBlockRow[], // or single block
  cachedAt: string,        // ISO timestamp
}
```

**Error Response**:

```typescript
{
  success: false,
  error: { code: string, message: string }
}
```

### 6.2 Admin Content Endpoints (Auth Required)

| Method   | Path                          | Description                              |
| -------- | ----------------------------- | ---------------------------------------- |
| `GET`    | `/api/admin/content`          | All blocks for a page                    |
| `GET`    | `/api/admin/content/:id`      | Single block by ID                       |
| `PUT`    | `/api/admin/content/:id`       | Update block (immediate, no draft state) |

**Notes**:
- `POST /api/admin/content` is **not exposed** to admin UI — new items are added via the `PUT` endpoint with an `addItem` operation on array-type blocks (subject to section cap)
- `DELETE` — Editors can remove items from array-type blocks via `PUT`. Hard deletion of entire blocks is developer-only via database migration.
- There is **no publish endpoint** — changes are live immediately on save

### 6.3 Media Endpoints (Auth Required)

| Method   | Path                      | Description                                         |
| -------- | ------------------------- | --------------------------------------------------- |
| `GET`    | `/api/admin/media`         | List media with pagination (`?page=1&limit=20&q=`)  |
| `POST`   | `/api/admin/media/upload`  | Upload file (multipart/form-data)                   |
| `DELETE` | `/api/admin/media/:id`     | Delete media (with usage check)                     |

### 6.4 Auth Endpoints

| Method | Path                | Description                 |
| ------ | ------------------- | --------------------------- |
| `POST` | `/api/auth/login`   | Login with email + password |
| `POST` | `/api/auth/refresh` | Refresh access token        |
| `GET`  | `/api/auth/me`      | Get current admin profile   |
| `POST` | `/api/auth/logout`  | Logout and invalidate token |

### 6.5 Admin System Endpoints (Super Admin)

| Method   | Path                    | Description                                                      |
| -------- | ----------------------- | ---------------------------------------------------------------- |
| `GET`    | `/api/admin/stats`       | Dashboard statistics                                             |
| `GET`    | `/api/admin/audit-log`   | Paginated audit log (`?page=1&limit=50&user=&action=&from=&to=`) |
| `GET`    | `/api/admin/admins`     | List all admin accounts                                          |
| `POST`   | `/api/admin/admins`     | Invite new admin                                                 |
| `PUT`    | `/api/admin/admins/:id` | Update admin (role, status)                                      |
| `DELETE` | `/api/admin/admins/:id` | Deactivate admin                                                 |

### 6.6 Update Operation Format

Since there is no create/delete API for entire blocks, item-level mutations use an operation format:

```typescript
// PUT /api/admin/content/:id

// Operation types for array blocks (addItem, removeItem, updateItem)
type ContentUpdateOperation =
  | { op: "update"; path: string; value: any }           // Update a field (e.g. hero.headline)
  | { op: "addItem"; path: string; value: any }           // Add to array (e.g. mentors.items)
  | { op: "removeItem"; path: string; index: number }    // Remove from array
  | { op: "replace"; path: string; value: any }          // Replace entire block data

// Full request body
interface UpdateRequest {
  operations: ContentUpdateOperation[];
}
```

---

## 7. Project Structure

### 7.1 Frontend (`volubit-frontend`)

```
volubit-frontend/
├── src/
│   ├── lib/
│   │   ├── api.ts                    # Typed API client (fetch wrapper)
│   │   ├── api-client.ts             # Runtime API client (reads env)
│   │   ├── content.ts                # Content fetching + TanStack Query hooks
│   │   ├── types/
│   │   │   └── content.ts           # All block type interfaces
│   │   └── static-defaults/          # Fallback content (SSR if BE unreachable)
│   │       ├── home.ts
│   │       ├── community.ts
│   │       ├── about.ts
│   │       ├── events.ts
│   │       ├── articles.ts
│   │       ├── faq.ts
│   │       ├── membership.ts
│   │       ├── partnership.ts
│   │       └── contact.ts
│   ├── components/
│   │   ├── home/
│   │   │   ├── sections.tsx          # Receives content as props
│   │   │   ├── Hero.tsx             # Receives content as props
│   │   │   └── CommunityShowcase.tsx
│   │   ├── site/
│   │   │   ├── Nav.tsx              # Reads site settings (logo, social links)
│   │   │   └── Footer.tsx           # Reads site settings (copyright, links)
│   │   └── ui/                      # shadcn/ui components (unchanged)
│   ├── hooks/
│   │   ├── useContentSection.ts     # TanStack Query hook per section
│   │   └── useSiteSettings.ts       # TanStack Query hook for global settings
│   ├── routes/
│   │   ├── __root.tsx               # Root layout (Nav + Footer + announcement bar)
│   │   ├── index.tsx                # Home page SSR content load
│   │   ├── community.tsx
│   │   ├── about.tsx
│   │   ├── events/
│   │   │   ├── index.tsx
│   │   │   └── past.tsx
│   │   ├── articles.tsx
│   │   ├── faq.tsx
│   │   ├── membership/
│   │   │   ├── index.tsx
│   │   │   └── pricing.tsx
│   │   ├── partnership.tsx
│   │   └── contact.tsx
│   └── styles.css
├── .env.example
└── package.json
```

### 7.2 Backend (`volubit-backend`)

```
volubit-backend/
├── src/
│   ├── routes/
│   │   ├── content/
│   │   │   ├── index.get.ts          # List all blocks (admin) or by page
│   │   │   ├── [page].get.ts         # Public: get page blocks
│   │   │   ├── [page]/[section].get.ts  # Public: get section
│   │   │   ├── [id].get.ts           # Get single block by ID (admin)
│   │   │   └── [id].put.ts           # Update block (admin)
│   │   ├── media/
│   │   │   ├── index.get.ts          # List media
│   │   │   ├── upload.post.ts        # Upload file
│   │   │   └── [id].delete.ts        # Delete media
│   │   ├── auth/
│   │   │   ├── login.post.ts
│   │   │   ├── refresh.post.ts
│   │   │   ├── me.get.ts
│   │   │   └── logout.post.ts
│   │   ├── admin/
│   │   │   ├── stats.get.ts
│   │   │   ├── audit-log.get.ts
│   │   │   ├── admins.get.ts
│   │   │   ├── admins.post.ts
│   │   │   └── admins.[id].put.ts
│   │   ├── site-settings/
│   │   │   ├── index.get.ts          # Public: site settings
│   │   │   └── index.put.ts          # Admin: update settings
│   │   └── internal/
│   │       └── invalidate.post.ts    # Cache invalidation webhook
│   ├── db/
│   │   ├── index.ts                  # DB connection
│   │   ├── schema.ts                 # Drizzle schema (all tables)
│   │   └── migrations/               # Drizzle migration files
│   ├── middleware/
│   │   ├── auth.ts                   # JWT verification middleware
│   │   ├── role.ts                  # Role-based access middleware
│   │   └── audit.ts                  # Audit log middleware
│   ├── services/
│   │   ├── content.service.ts        # Business logic for content updates
│   │   ├── media.service.ts          # Business logic for media
│   │   ├── auth.service.ts           # Auth logic (password hashing, JWT)
│   │   └── cache.service.ts          # Cache invalidation logic
│   ├── storage/
│   │   └── index.ts                  # File storage abstraction (local / R2 / S3)
│   ├── utils/
│   │   ├── response.ts               # Typed API response helpers
│   │   └── validators.ts             # Zod schemas for request validation
│   └── index.ts                      # App entry
├── drizzle.config.ts
├── package.json
└── .env.example
```

---

## 8. Database Schema

### 8.1 Tables Overview

```
content_blocks       CMS content blocks (structure defined at seed time)
admins               admin user accounts
media                uploaded files
audit_log            all admin actions
```

### 8.2 content_blocks

```sql
CREATE TABLE content_blocks (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  page          TEXT NOT NULL,        -- 'home' | 'community' | 'about' | etc.
  section       TEXT NOT NULL,        -- 'hero' | 'upcoming-events' | etc.
  block_type    TEXT NOT NULL,        -- 'HeroBlock' | 'CardGridBlock' | etc.
  locale        TEXT NOT NULL DEFAULT 'en',
  data          JSONB NOT NULL,
  created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_by    UUID REFERENCES admins(id),
  UNIQUE (page, section, locale)
);
```

Note: No `status` column — all blocks are always live. No `current_version` — no versioning.

### 8.3 admins

```sql
CREATE TABLE admins (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email         TEXT UNIQUE NOT NULL,
  name          TEXT NOT NULL,
  password_hash TEXT NOT NULL,
  role          TEXT NOT NULL DEFAULT 'editor'
                  CHECK (role IN ('editor', 'admin', 'super_admin')),
  is_active     BOOLEAN NOT NULL DEFAULT true,
  last_login    TIMESTAMPTZ,
  created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at    TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```

### 8.4 media

```sql
CREATE TABLE media (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  filename      TEXT NOT NULL,
  original_name TEXT NOT NULL,
  url           TEXT NOT NULL,
  mime_type     TEXT NOT NULL,
  size          INT NOT NULL,
  width         INT,
  height        INT,
  alt           TEXT,
  uploaded_by   UUID REFERENCES admins(id),
  created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```

### 8.5 audit_log

```sql
CREATE TABLE audit_log (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  admin_id      UUID REFERENCES admins(id),
  action        TEXT NOT NULL,       -- 'update' | 'upload' | 'delete' | 'login'
  block_id      UUID,
  block_page    TEXT,
  block_section TEXT,
  metadata      JSONB,
  ip_address    TEXT,
  created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- audit_log is append-only; no UPDATE or DELETE allowed via application
```

---

## 9. User Flows

### 9.1 Content Editor Updates a Mentor's Name

```
1. Editor logs in at admin.volubit.io
2. Clicks "Home" in the sidebar → expands to show all sections
3. Clicks "Mentors / Instructors" section
4. Section view shows 3 mentor cards in a fixed list (no drag handles)
5. Editor clicks the first mentor card "Daniel Wijaya"
6. Slide-over editor opens with form fields
7. Editor changes "Daniel Wijaya" to "Daniel Wijaya, CFA"
8. Editor clicks "Save"
9. Confirmation: "Changes saved and published immediately"
10. Audit log entry created
11. FE cache invalidated → Public site shows updated name
```

### 9.2 Content Editor Adds a New Mentor

```
1. Editor opens "Mentors" section
2. Clicks "+ Add Mentor" button (button is disabled if cap is reached — 10 mentors)
3. Slide-over opens with empty mentor form
4. Editor fills: name, role, experience, specialization, courses, certifications
5. Editor clicks "Upload Photo" — file picker opens, selects image.jpg
6. Image uploaded to /api/admin/media/upload, returns URL
7. Photo field populated with thumbnail preview
8. Editor clicks "Save"
9. New mentor appears at the end of the mentor list (fixed position)
10. Audit log entry created
11. FE cache invalidated
```

### 9.3 Content Editor Removes an Event

```
1. Editor opens "Upcoming Events" section
2. Clicks the event card to expand it
3. Clicks "Remove" button
4. Confirmation dialog: "Remove this event? This cannot be undone."
5. Editor confirms
6. Item removed from the section's items array
7. Audit log entry created
8. FE cache invalidated
```

### 9.4 Content Editor Uploads a Hero Image

```
1. Editor opens "Home" → "Hero" section
2. Clicks on the hero image field
3. "Upload Image" or "Choose from Library" modal appears
4. Editor clicks "Upload" → selects hero-bg.jpg from file picker
5. File uploaded to /api/admin/media/upload
6. BE returns: { url: "https://media.volubit.io/hero-bg.jpg", width: 1920, height: 1080 }
7. Image preview populated in the field
8. Editor clicks "Save"
9. Audit log entry created
10. Public site hero image updated
```

### 9.5 Public Visitor Page Load

```
1. User types volubit.io/events
2. TanStack Start server receives request
3. Server calls GET https://api.volubit.io/api/content/events
4. BE returns all blocks for events page (cached 5 min)
5. Server renders full HTML with content (SSR)
6. Full HTML sent to user (< 200ms target)
7. React hydrates on client
8. Subsequent client-side navigation uses TanStack Query cache
```

### 9.6 Admin Resizes a Gallery Image

```
1. Editor opens "Home" → "Community Gallery" section
2. Gallery section shows 11 image thumbnails
3. Editor clicks an image thumbnail
4. Image editor modal opens with: src preview, alt text field, layout hint selector
5. Editor changes alt text to "Members at Volubit meetup"
6. Editor changes layout hint from "portrait" to "landscape"
7. Editor clicks "Save"
8. Changes saved immediately
9. FE cache invalidated
```

---

## 10. Implementation Plan

### Phase 1 — Backend Foundation

**Timeline**: Week 1–2

| Task                     | Description                                                            |
| ------------------------ | ---------------------------------------------------------------------- |
| BE project setup         | Initialize Nitro project, configure TypeScript, set up environment     |
| Database setup           | Spin up PostgreSQL, configure Drizzle ORM, write all migration files   |
| DB schema implementation | Create all 4 tables, indexes, constraints                              |
| Admin auth               | Login endpoint, password hashing (bcrypt), JWT issuance, refresh logic  |
| Auth middleware          | JWT verification middleware for all admin routes                       |
| Content CRUD API         | Read + Update for content_blocks (no create/delete for blocks)         |
| Media upload API         | File upload handler, local storage, media table                       |
| Audit log                | Middleware that logs every admin mutation                              |
| Seed data                | Populate DB with current hardcoded content as initial blocks           |

**Deliverable**: A working BE API where all content blocks can be read and updated immediately. No admin UI yet.

---

### Phase 2 — Admin Panel Core

**Timeline**: Week 2–3

| Task                   | Description                                                      |
| ---------------------- | ---------------------------------------------------------------- |
| Admin login page       | Email + password form, JWT storage, session management           |
| Dashboard              | Block count per page, recent edits, media usage stats            |
| Page browser sidebar   | Collapsible page list with section tree                          |
| Section editor view    | List view of items (fixed order — no drag handles), item preview |
| Inline item editor     | Form modal/slide-over for all block types                        |
| Add/Remove items        | "Add Item" button with cap enforcement, "Remove" per item         |
| Save action            | Immediate save, confirmation, audit log entry                    |
| Image upload in editor | Upload flow from within item editor (modal or inline)             |

**Deliverable**: A functional admin panel where a logged-in editor can navigate pages, edit section items, add/remove items within cap, and save changes.

---

### Phase 3 — Frontend Integration

**Timeline**: Week 3–4

| Task                  | Description                                        |
| --------------------- | -------------------------------------------------- |
| API client            | Typed fetch wrapper with auth header injection      |
| Content hooks         | TanStack Query hooks per block type                 |
| Static defaults       | Write fallback files for all pages                 |
| Refactor sections.tsx | Replace hardcoded arrays with content hook calls    |
| Refactor route pages  | Update all 9 pages to SSR-load content             |
| Nav + Footer          | Integrate site settings (logo, social, footer)     |
| Cache invalidation    | On admin save → invalidate FE cache                |

**Deliverable**: Frontend is fully content-driven from BE API. Static defaults ensure site is never empty. Zero visual regressions.

---

### Phase 4 — Polish & Admin Management

**Timeline**: Week 4–5

| Task                     | Description                                     |
| ------------------------ | ----------------------------------------------- |
| Media library            | Standalone media grid with search, delete       |
| Rich text editor         | TipTap integration for RichTextBlock fields      |
| Site settings panel      | Global settings form                            |
| Role-based access        | Editor vs Admin vs Super Admin enforcement       |
| Admin account management | Invite, deactivate, role change UI             |
| Audit log viewer         | Paginated log table with filters                |
| Error handling           | BE errors don't crash FE (fallback to static)   |
| Performance audit        | SSR timing, API response times, cache hit rates  |

**Deliverable**: Full CMS system ready for production use.

---

## 11. Success Metrics

| Metric                    | Target                                | Measurement                |
| ------------------------- | ------------------------------------- | -------------------------- |
| Time to update content    | < 2 minutes from login to live        | Observed from content team |
| CMS content coverage      | 100% of static data is CMS-controlled | Audit of all sections      |
| Zero deploys for content  | 100% of content updates via admin     | Deploy frequency tracking  |
| FE page load time (SSR)   | < 300ms (TTFB)                        | Lighthouse / WebPageTest   |
| BE API response time      | < 100ms p95                           | API monitoring             |
| Cache hit rate            | > 90% on public API                   | BE cache metrics           |
| Admin panel load time     | < 2 seconds initial load              | Browser dev tools          |

---

## 12. Open Decisions

These decisions need to be made before implementation begins:

### 12.1 Backend Hosting

Where should `volubit-backend` run?

- **Option A**: Fly.io or Railway — simple, fast deploy, pays for usage
- **Option B**: Supabase managed Postgres + Edge Functions, generous free tier
- **Option C**: Cloudflare Workers (Durable Objects) — global edge, pay-per-request
- **Decision needed**: Before Phase 1

### 12.2 Media Storage

Where should uploaded images be stored?

- **Option A**: Local filesystem (Nitro public folder) — simplest, but not production-scale
- **Option B**: Cloudflare R2 — S3-compatible, no egress fees, cheap
- **Option C**: AWS S3 — familiar, standard, egress costs
- **Decision needed**: Before Phase 2

### 12.3 Frontend Hosting

Where should `volubit-frontend` deploy?

- **Current state**: Appears to be a self-hosted Node.js app (Nitro server.ts)
- **Option A**: Vercel — best TanStack Start support, edge network, preview deployments
- **Option B**: Cloudflare Pages — free, fast, Workers integration
- **Option C**: Fly.io — keep self-hosted, full control
- **Decision needed**: Before Phase 3

### 12.4 BE Project Stack

Keep Nitro or switch?

- **Option A**: Keep Nitro (same stack as FE) — team familiarity, unified DX
- **Option B**: Supabase managed backend — less custom code, faster to ship
- **Decision needed**: Before Phase 1

### 12.5 Auth Model

Simple shared password or individual accounts?

- **Option A**: Shared admin password (one password for all editors) — simplest
- **Option B**: Individual accounts per admin (email + password) — auditable, role-managed
- **Recommendation**: Individual accounts (Option B) — the audit log is only meaningful with per-user auth

---

_End of PRD v1.0_
