From 0a97b679a6f5e919487bd3abfc82b2573261d34a Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sat, 31 Jan 2026 22:32:52 -0500 Subject: [PATCH] docs: Add hybrid SSR and project guidelines documentation - Document hybrid SSR architecture for frontend implementation - Add project guidelines for development workflow - Explain API preservation and SSR approach --- HYBRID_SSR_IMPLEMENTATION.md | 703 +++++++++++++++++++++++++++++++++++ PROJECT_GUIDELINES.md | 176 +++++++++ 2 files changed, 879 insertions(+) create mode 100644 HYBRID_SSR_IMPLEMENTATION.md create mode 100644 PROJECT_GUIDELINES.md diff --git a/HYBRID_SSR_IMPLEMENTATION.md b/HYBRID_SSR_IMPLEMENTATION.md new file mode 100644 index 0000000..837a2a8 --- /dev/null +++ b/HYBRID_SSR_IMPLEMENTATION.md @@ -0,0 +1,703 @@ +# Hybrid SSR + API Implementation Guide + +## Executive Summary + +This guide documents a **Hybrid Server-Side Rendering (SSR) + API** architecture that: +- ✅ **Keeps all existing API routes unchanged** (`/api/*` routes remain intact) +- ✅ **Adds new HTML routes** (`/collections`, `/collections/:id`) that fetch data server-side +- ✅ **Shares business logic** between API and HTML routes via the service layer +- ✅ **Provides faster initial page loads** while maintaining API flexibility +- ✅ **Zero breaking changes** to mobile apps, external consumers, or existing API clients + +## Current Architecture (Analysis) + +### Existing Pattern: API-Driven Frontend + +**Current structure:** +``` +Browser visits /dashboard + ↓ +Go template renders EMPTY HTML skeleton + ↓ +JavaScript fetch() calls /api/libraries/visible + ↓ +API returns JSON data + ↓ +JavaScript populates the DOM +``` + +**Example from current codebase:** + +**cmd/server/main.go:309-328:** +```go +protected.GET("/dashboard", func(c echo.Context) error { + // Get user from JWT context + userID := c.Get("user_id").(string) + user := templates.User{ + ID: userID, + Email: c.Get("user_email").(string), + Username: c.Get("user_username").(string), + Role: c.Get("user_role").(string), + } + + // Render template WITH NO DATA + var buf bytes.Buffer + err := templates.Dashboard(user).Render(c.Request().Context(), &buf) + return c.HTML(http.StatusOK, buf.String()) +}) +``` + +**templates/dashboard.templ:** +```javascript +function loadLibraries() { + // Template is empty, fetch data via API + fetch('/api/libraries/visible', { + headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') } + }) + .then(response => response.json()) + .then(data => { + libraries = data; + renderLibraries(); + }) +} +``` + +### Service Layer Architecture (Already Well-Designed) + +The codebase **already uses a service layer**, which is perfect for the hybrid approach: + +``` +API Handler + ↓ calls +Service Layer (business logic) + ↓ queries +Database Layer +``` + +**Example from internal/handlers/collections.go:100-107:** +```go +func (h *CollectionHandler) GetCollections(c echo.Context) error { + user := c.Get("user").(database.Users) + userUUID := uuid.UUID(user.ID.Bytes) + + // Business logic is in the SERVICE layer + collections, err := h.collectionService.GetUserCollections( + c.Request().Context(), + userUUID + ) + + return c.JSON(http.StatusOK, map[string]interface{}{ + "collections": collections, + "total": len(collections), + }) +} +``` + +**Service layer (internal/services/collection_service.go):** +```go +func (s *CollectionService) GetUserCollections(ctx context.Context, userID uuid.UUID) ([]database.GetCollectionsRow, error) { + // Business logic, validation, database queries + return s.db.GetCollections(ctx, database.GetCollectionsParams{ + UserID: pgtype.UUID{Bytes: userID, Valid: true}, + }) +} +``` + +**This is the key insight: The service layer (`GetUserCollections`) contains all the business logic. The handler just formats the response (JSON vs HTML).** + +## Proposed Hybrid Architecture + +### New Pattern: SSR + API + +``` +Browser visits /collections + ↓ +Go fetches data from service layer + ↓ +Go template renders HTML WITH DATA + ↓ +Browser shows complete page instantly ⚡ + ↓ +(Optional) JavaScript updates via API for interactivity +``` + +**Key principle: Shared service layer, different response formats** + +``` +┌─────────────────────────────────────────┐ +│ BOTH routes call SAME service method │ +└─────────────────────────────────────────┘ + ↓ ↓ + /api/collections /collections + (JSON response) (HTML response) + ↓ ↓ + c.JSON(200, data) render(template, data) +``` + +## Implementation Guide + +### What Changes (and What Doesn't) + +| Component | Changes? | Why | +|-----------|----------|-----| +| **Service Layer** | ❌ No change | Already well-designed | +| **Database Queries** | ❌ No change | Already optimized | +| **Business Logic** | ❌ No change | Single source of truth | +| **`/api/*` routes** | ❌ No change | Keep API intact | +| **Template signatures** | ✅ Yes | Add data parameters | +| **New HTML routes** | ✅ Yes | Add `/collections`, etc. | +| **Template JavaScript** | ✅ Yes | Remove initial fetch() | + +### Step 1: Add Helper Methods to Handlers (Optional but Recommended) + +**Purpose:** Extract data-fetching logic so both API and HTML routes can use it. + +**Example for collections.go:** + +```go +// Add this method to CollectionHandler +// Returns raw data (not JSON, not HTML) +func (h *CollectionHandler) GetCollectionsData(c echo.Context) ([]database.GetCollectionsRow, error) { + user := c.Get("user").(database.Users) + userUUID := uuid.UUID(user.ID.Bytes) + + collections, err := h.collectionService.GetUserCollections( + c.Request().Context(), + userUUID, + ) + + return collections, err +} + +// Now API handler uses this helper +func (h *CollectionHandler) GetCollections(c echo.Context) error { + collections, err := h.GetCollectionsData(c) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + // Format as JSON + return c.JSON(http.StatusOK, map[string]interface{}{ + "collections": collections, + "total": len(collections), + }) +} +``` + +**Why this pattern:** +- API handler: calls `GetCollectionsData()` → returns JSON +- HTML route: calls `GetCollectionsData()` → returns HTML +- Single source of truth, no duplication + +### Step 2: Add New HTML Routes + +**Location:** `cmd/server/main.go` + +**Add these new routes after the existing `/api/*` routes:** + +```go +// ===== NEW HTML ROUTES (SSR) ===== +// These routes fetch data server-side and render pre-populated templates +// The /api/* routes remain unchanged for JSON API consumers + +// Collections list page with SSR +protected.GET("/collections", func(c echo.Context) error { + user := getUserFromContext(c) + + // Fetch data server-side + collections, err := collectionHandler.GetCollectionsData(c) + if err != nil { + return c.HTML(http.StatusInternalServerError, "Error loading collections") + } + + // Convert to template format if needed + type CollectionData struct { + ID string + Name string + Description string + Color string + Icon string + } + templateData := make([]CollectionData, len(collections)) + for i, col := range collections { + templateData[i] = CollectionData{ + ID: uuid.UUID(col.ID.Bytes).String(), + Name: col.Name, + Description: textToString(col.Description), + Color: textToString(col.Color), + Icon: textToString(col.Icon), + } + } + + // Render template WITH data + var buf bytes.Buffer + err = templates.Collections(user, templateData).Render(c.Request().Context(), &buf) + if err != nil { + return err + } + return c.HTML(http.StatusOK, buf.String()) +}) + +// Collection detail page with SSR +protected.GET("/collections/:id", func(c echo.Context) error { + user := getUserFromContext(c) + collectionID, err := uuid.Parse(c.Param("id")) + if err != nil { + return c.HTML(http.StatusBadRequest, "Invalid collection ID") + } + + // Fetch collection data server-side + collection, err := collectionHandler.GetCollectionData(c, collectionID) + if err != nil { + return c.HTML(http.StatusNotFound, "Collection not found") + } + + // Fetch books in collection + books, err := collectionHandler.GetCollectionBooksData(c, collectionID) + if err != nil { + return c.HTML(http.StatusInternalServerError, "Error loading books") + } + + // Render template WITH data + var buf bytes.Buffer + err = templates.CollectionDetail(user, collection, books).Render(c.Request().Context(), &buf) + if err != nil { + return err + } + return c.HTML(http.StatusOK, buf.String()) +}) +``` + +**Important notes:** +- These are **NEW routes**, they don't replace `/api/collections` +- The `/api/collections` route remains unchanged +- Mobile apps and external consumers continue using `/api/*` +- Only browsers visiting `/collections` get the SSR version + +### Step 3: Update Template Signatures + +**Current template signature:** +```templ +templ Collections(user User) { +
+ +} +``` + +**New template signature:** +```templ +templ Collections(user User, collections []Collection) { +
+ for col := range collections { +
+

{ col.Name }

+

{ col.Description }

+
+ } +
+ +} +``` + +### Step 4: Helper Function for User Context + +**Add this utility function to main.go to reduce boilerplate:** + +```go +// Helper to get user from JWT context +func getUserFromContext(c echo.Context) templates.User { + return templates.User{ + ID: c.Get("user_id").(string), + Email: c.Get("user_email").(string), + Username: c.Get("user_username").(string), + Role: c.Get("user_role").(string), + } +} +``` + +**Now all HTML routes can use this:** +```go +protected.GET("/collections", func(c echo.Context) error { + user := getUserFromContext(c) // Reusable helper + // ... rest of the code +}) +``` + +## Route Structure Summary + +### Before (Current) + +``` +/api/collections → JSON (API) +/api/collections/:id → JSON (API) +/dashboard → HTML (no SSR, fetches data) +/devices → HTML (no SSR, fetches data) +``` + +### After (Hybrid) + +``` +/api/collections → JSON (API) - UNCHANGED +/api/collections/:id → JSON (API) - UNCHANGED +/collections → HTML (SSR) - NEW +/collections/:id → HTML (SSR) - NEW +/dashboard → HTML (no SSR) - UNCHANGED +/devices → HTML (no SSR) - UNCHANGED +``` + +## Migration Checklist + +For each new page you want to convert to SSR: + +- [ ] **Add data-fetching helper** to handler (if doesn't exist) +- [ ] **Create new HTML route** in `cmd/server/main.go` +- [ ] **Update template signature** to accept data +- [ ] **Remove initial fetch()** from template JavaScript +- [ ] **Keep fetch() for CRUD operations** (create, update, delete) +- [ ] **Test API routes still work** with curl/Postman +- [ ] **Test HTML routes** in browser + +## Benefits + +### Performance + +**Before (API-only):** +1. Browser requests page +2. Server renders empty HTML (~10ms) +3. Browser receives HTML +4. JavaScript fetches data (~50ms) +5. Server queries database (~20ms) +6. Server returns JSON (~5ms) +7. Browser renders data (~10ms) +**Total: ~95ms visible to user** + +**After (SSR):** +1. Browser requests page +2. Server queries database (~20ms) +3. Server renders HTML with data (~10ms) +4. Browser receives complete HTML +5. Browser paints page (~10ms) +**Total: ~40ms visible to user** + +**58% faster initial load!** + +### Architectural Benefits + +1. **Zero logic duplication:** Service layer is single source of truth +2. **API remains intact:** No breaking changes for mobile/external consumers +3. **SEO friendly:** Search engines see complete HTML +4. **Progressive enhancement:** Works without JavaScript +5. **Easier testing:** Can test API routes independently + +## Example: Complete Implementation + +### File: internal/handlers/collections.go + +**Add these helper methods:** + +```go +// GetCollectionsData returns raw collection data (for both API and HTML) +func (h *CollectionHandler) GetCollectionsData(c echo.Context) ([]database.GetCollectionsRow, error) { + user := c.Get("user").(database.Users) + userUUID := uuid.UUID(user.ID.Bytes) + + return h.collectionService.GetUserCollections( + c.Request().Context(), + userUUID, + ) +} + +// GetCollectionData returns single collection (for both API and HTML) +func (h *CollectionHandler) GetCollectionData(c echo.Context, collectionID uuid.UUID) (database.Collections, error) { + return h.collectionService.GetCollection(c.Request().Context(), collectionID) +} + +// GetCollectionBooksData returns books in collection (for both API and HTML) +func (h *CollectionHandler) GetCollectionBooksData(c echo.Context, collectionID uuid.UUID) ([]database.GetCollectionBooksRow, error) { + return h.collectionService.GetCollectionBooks(c.Request().Context(), collectionID) +} +``` + +**Update existing API handlers to use helpers:** + +```go +// Before +func (h *CollectionHandler) GetCollections(c echo.Context) error { + user := c.Get("user").(database.Users) + userUUID := uuid.UUID(user.ID.Bytes) + collections, err := h.collectionService.GetUserCollections(...) + // ... +} + +// After +func (h *CollectionHandler) GetCollections(c echo.Context) error { + collections, err := h.GetCollectionsData(c) + // Format and return JSON + // ... +} +``` + +### File: cmd/server/main.go + +**Add after line 258 (after collections API routes):** + +```go +// ===== HTML ROUTES WITH SSR ===== +// These provide fast initial page loads while keeping /api/* routes intact + +// Collections list page +protected.GET("/collections", func(c echo.Context) error { + user := getUserFromContext(c) + + collections, err := collectionHandler.GetCollectionsData(c) + if err != nil { + return c.HTML(500, "Error loading collections") + } + + type ColData struct { + ID string + Name string + Description string + Color string + Icon string + } + + data := make([]ColData, len(collections)) + for i, col := range collections { + data[i] = ColData{ + ID: uuid.UUID(col.ID.Bytes).String(), + Name: col.Name, + Description: textToString(col.Description), + Color: textToString(col.Color), + Icon: textToString(col.Icon), + } + } + + var buf bytes.Buffer + templates.Collections(user, data).Render(c.Request().Context(), &buf) + return c.HTML(200, buf.String()) +}) + +// Collection detail page +protected.GET("/collections/:id", func(c echo.Context) error { + user := getUserFromContext(c) + collectionID, _ := uuid.Parse(c.Param("id")) + + collection, err := collectionHandler.GetCollectionData(c, collectionID) + if err != nil { + return c.HTML(404, "Collection not found") + } + + books, err := collectionHandler.GetCollectionBooksData(c, collectionID) + if err != nil { + return c.HTML(500, "Error loading books") + } + + var buf bytes.Buffer + templates.CollectionDetail(user, collection, books).Render(c.Request().Context(), &buf) + return c.HTML(200, buf.String()) +}) +``` + +### File: templates/collections.templ + +**Update template signature:** + +```templ +// Before +templ Collections(user User) { +
+ +} + +// After +templ Collections(user User, collections []Collection) { +
+ for col := range collections { +
+
+
+

{ col.Name }

+

{ col.Description }

+
+
+ + + +
+
+
+ } +
+ + +} +``` + +## Testing Checklist + +### API Routes (Must Remain Unchanged) + +```bash +# Test collections API +curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8765/api/collections +# Should return JSON as before + +curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8765/api/collections/ID +# Should return JSON as before +``` + +### HTML Routes (New) + +```bash +# Test HTML route in browser +open http://localhost:8765/collections +# Should show complete page instantly with data +``` + +### Verify No Breaking Changes + +1. **Mobile apps still work:** Test with existing mobile app or API client +2. **External consumers still work:** Test with Postman/curl +3. **Database queries unchanged:** Monitor query logs +4. **Business logic unchanged:** Test collection creation, updates, deletion + +## Common Pitfalls + +### ❌ Don't: Modify Existing API Routes + +```go +// WRONG: Don't change existing API handler +func (h *CollectionHandler) GetCollections(c echo.Context) error { + // Don't add HTML rendering here! + if wantsHTML(c) { + return renderHTML(...) + } + return c.JSON(...) +} +``` + +**Why:** Breaks API consumers, adds complexity to API routes. + +### ✅ Do: Create Separate HTML Routes + +```go +// RIGHT: Keep API handler simple, add separate HTML route +func (h *CollectionHandler) GetCollections(c echo.Context) error { + data, _ := h.GetCollectionsData(c) + return c.JSON(200, data) // JSON only +} + +// Separate HTML route in main.go +protected.GET("/collections", func(c echo.Context) error { + data, _ := collectionHandler.GetCollectionsData(c) + return render(c, templates.Collections(user, data)) +}) +``` + +**Why:** Clean separation, API stays simple, HTML routes are independent. + +### ❌ Don't: Duplicate Business Logic + +```go +// WRONG: Duplicating queries +func (h *CollectionHandler) GetCollections(c echo.Context) error { + collections, _ := h.db.GetCollections(...) // Query here + return c.JSON(200, collections) +} + +protected.GET("/collections", func(c echo.Context) error { + collections, _ := h.db.GetCollections(...) // Same query again! + return render(c, templates.Collections(user, collections)) +}) +``` + +**Why:** Duplication, harder to maintain, bugs appear twice. + +### ✅ Do: Share Service Layer + +```go +// RIGHT: Both use same service method +func (h *CollectionHandler) GetCollectionsData(c echo.Context) { + return h.collectionService.GetUserCollections(...) // Single source +} + +func (h *CollectionHandler) GetCollections(c echo.Context) error { + data, _ := h.GetCollectionsData(c) + return c.JSON(200, data) +} + +protected.GET("/collections", func(c echo.Context) error { + data, _ := collectionHandler.GetCollectionsData(c) + return render(c, templates.Collections(user, data)) +}) +``` + +**Why:** Single source of truth, DRY principle, easier maintenance. + +## Rollback Plan + +If anything goes wrong, rollback is trivial: + +1. **Remove new HTML routes:** Delete the `protected.GET("/collections", ...)` blocks from main.go +2. **Revert template changes:** Change `templ Collections(user, collections)` back to `templ Collections(user)` +3. **API routes untouched:** No changes made to `/api/*` routes + +**Zero risk to existing functionality.** + +## Next Steps + +This document provides the architectural foundation. Before implementing Phase 9 (Collections UI), the codebase should be migrated to this hybrid pattern for consistency: + +1. Migrate existing pages (`/dashboard`, `/devices`) to use SSR +2. Add helper methods to all handlers +3. Update all templates to accept data +4. Test thoroughly + +Once the pattern is established, Phase 9 implementation will be straightforward and follow the same conventions. + +--- + +**Document Version:** 1.0 +**Last Updated:** 2025-01-31 +**Status:** Ready for Implementation diff --git a/PROJECT_GUIDELINES.md b/PROJECT_GUIDELINES.md new file mode 100644 index 0000000..52dfeb9 --- /dev/null +++ b/PROJECT_GUIDELINES.md @@ -0,0 +1,176 @@ +# Combined Project Guidelines for Bookmann + +## 🚨 CRITICAL PROHIBITIONS (Never violate these) + +### Backend & Database +- ❌ **NEVER modify backend code when working on frontend-only tasks** +- ❌ **NEVER modify database schema** unless explicitly instructed for full-stack changes +- ❌ **NEVER use Docker** - use Podman only +- ❌ **NEVER build server binaries locally** - all builds through Dockerfile/docker-compose +- ❌ **NEVER create new migration files** - merge changes into current one until release +- ❌ **NEVER use `git checkout` on schema files** without checking what will be lost +- ❌ **NEVER break existing functionality** unless explicitly instructed + +### Frontend & Styling +- ❌ **NEVER modify backend/API for frontend features without user confirmation** +- ❌ **NEVER use custom CSS** - TailwindCSS classes only +- ❌ **NEVER use JavaScript** - convert all to TypeScript +- ❌ **NEVER use object-oriented programming** patterns - use functional/other paradigms +- ❌ **NEVER add new Dockerfiles without user confirmation** + +### General +- ❌ **NEVER skip pre-commit hooks** unless explicitly requested +- ❌ **NEVER force push to main/master** branches +- ❌ **NEVER commit files with secrets** (.env, credentials.json, etc.) +- ❌ **NEVER make assumptions** - ask clarifying questions when uncertain + +--- + +## 🎯 CONTEXT-SPECIFIC RULES + +### When Working on Frontend-Only Tasks +- **DO NOT touch backend code** - handlers, services, database layer +- **DO NOT modify API routes** - use existing endpoints only +- **DO NOT change database schema** - work with existing structure +- **If backend change seems necessary**: + 1. Identify the required change + 2. Explain why you need it + 3. Provide impact analysis + 4. **ASK FOR USER CONFIRMATION before proceeding** + +### When Working on Full-Stack Tasks +- Backend changes are allowed when explicitly part of the task +- Still follow all database protocols (atomic changes, validation, etc.) +- Still use Podman for all builds +- Still include Bruno tests for API changes + +--- + +## ✅ MANDATORY REQUIREMENTS + +### Database Operations (Full-Stack Tasks Only) +- ✅ Follow **pgx v5 standards** for all database operations +- ✅ Treat schema changes as **ATOMIC** - complete success or complete rejection +- ✅ When schema changes occur: delete database and rebuild with clean Podman cache +- ✅ Use **pre-change checklist**: read schema → identify columns → plan changes → verify → read back +- ✅ **Post-change validation**: ensure schema.sql, models.go, and queries.sql are in sync + +### Build & Deployment +- ✅ Use **Podman** exclusively (not Docker) +- ✅ All builds through existing **Dockerfile** and **docker-compose.yml** +- ✅ Stop building server binaries - everything goes through containers + +### API Changes (Full-Stack Tasks Only) +- ✅ Include **Bruno v3.0 .bru requests** with all API documentation +- ✅ Tests must be **comprehensive and cover three contexts**: no user, user, and admin +- ✅ Maintain backward compatibility for mobile apps and external consumers + +### Frontend & Styling +- ✅ Always use **TailwindCSS classes** for all styling +- ✅ Convert all JavaScript to **TypeScript** +- ✅ Avoid OOP patterns - prefer functional/other paradigms + +### Code Organization +- ✅ Minimize project structure changes +- ✅ Place new files in **contextually appropriate directories** +- ✅ Follow **KISS**, **DRY**, and **YAGNI** principles +- ✅ Use **multiple, logical git commits** with clear messages + +### Configuration & Environment +- ✅ If **.env is missing**, auto-generate secure values +- ✅ Never commit secrets to repository + +### Documentation +- ✅ Update **README.md** when users/admins need to be informed +- ✅ Document API changes with Bruno collections + +### Process & Continuity +- ✅ If mid-task and receive "no response", **continue the task** +- ✅ Verify no regressions before modifying/removing code + +--- + +## 🔧 TECHNICAL STANDARDS + +### Backend Stack +- **Language**: Go 1.25+ +- **Database**: PostgreSQL 15+ with **pgx v5 driver** only +- **Authentication**: JWT tokens with bcrypt password hashing +- **Architecture**: Service layer pattern (handlers → services → database) + +### Frontend Stack +- **Styling**: TailwindCSS (no custom CSS) +- **Language**: TypeScript (no JavaScript) +- **Templates**: HTMX with server-side rendering +- **Patterns**: Functional/other (no OOP) + +### Containerization +- **Runtime**: Podman (not Docker) +- **Build**: Existing Dockerfile and docker-compose.yml only +- **No local builds** allowed + +--- + +## 📋 WORKFLOW CHECKLISTS + +### Before Making Frontend-Only Changes +- [ ] Identify if backend modification could make implementation simpler +- [ ] Plan to use existing API endpoints only +- [ ] If backend change seems necessary, prepare confirmation request: + - Required change description + - Why it would help + - Impact analysis + - Alternative approaches considered +- [ ] Plan git commit structure (multiple logical commits) +- [ ] Identify if README.md needs updates + +### Before Making Full-Stack Changes +- [ ] Read current schema completely (if database changes) +- [ ] Identify all columns that must be preserved +- [ ] Plan exact changes needed +- [ ] Verify Podman will be used for builds +- [ ] Plan git commit structure (multiple logical commits) + +### During Schema Changes (Full-Stack Only) +- [ ] Read current schema completely +- [ ] Identify all columns that must be preserved +- [ ] Plan exact changes needed +- [ ] Set up verification step +- [ ] Make intended changes +- [ ] Immediately verify by reading back modified sections +- [ ] Confirm ALL expected columns are present +- [ ] Verify schema.sql, models.go, and queries.sql are in sync + +### After API Changes +- [ ] Create/update Bruno v3.0 .bru requests +- [ ] Test with no user context +- [ ] Test with regular user context +- [ ] Test with admin context +- [ ] Verify backward compatibility + +### Before Committing +- [ ] Run tests: `go test ./... -v` +- [ ] Run lint/typecheck if available +- [ ] Ensure no secrets in changes +- [ ] Verify logical commit structure +- [ ] Update README.md if user-facing changes + +--- + +## 🏗 ARCHITECTURAL PATTERNS + +### Current: API-Driven Frontend +``` +Browser → Go template (empty) → JavaScript fetch() → API → Database +``` + +### Future Reference: Hybrid SSR (NOT TO IMPLEMENT YET) +``` +Browser → Go template (with data) → Display instantly + ↓ + JavaScript only for interactivity (CRUD) + ↓ + Shared service layer +``` + +Ultimately, whenever you are unsure just ask for confirmation.