Skip to content

Diskover CMS Architecture

Digital storefront platform for selling digital products and memberships.

Technology Stack

Backend

  • Go 1.23 - Primary language
  • Chi - HTTP router
  • pgx/v5 - PostgreSQL driver
  • slog - Structured logging (stdlib)
  • oapi-codegen - OpenAPI code generation
  • testify - Testing framework
  • Clerk SDK - Authentication

Frontend

  • React 18 - UI library
  • TanStack Router - Type-safe routing
  • TanStack Query - Data fetching
  • Clerk - Authentication
  • shadcn/ui - UI component library
  • Vite - Build tool
  • Orval - API client generation

Infrastructure

  • PostgreSQL 16 - Primary database
  • Docker Compose - Local development

Project Structure

diskover-app/
├── api/                        # OpenAPI specification
│   ├── openapi.yaml            # Main spec (entry point)
│   ├── openapi.bundled.yaml    # Bundled spec (generated)
│   ├── paths/                  # Endpoint definitions
│   │   ├── admin.yaml
│   │   ├── health.yaml
│   │   ├── orgs.yaml
│   │   ├── products.yaml
│   │   ├── transactions.yaml
│   │   └── users.yaml
│   └── components/schemas/     # Reusable schema definitions
│       ├── common.yaml
│       ├── orgs.yaml
│       ├── products.yaml
│       ├── transactions.yaml
│       └── users.yaml
├── backend/
│   ├── cmd/api/main.go         # Application entry point
│   └── internal/
│       ├── api/                # Generated code & router
│       │   ├── generated.go    # oapi-codegen output
│       │   └── router.go       # Chi router setup
│       ├── config/             # Configuration loading
│       ├── db/                 # Database layer
│       │   ├── client.go       # Repository aggregator
│       │   ├── migrate.go      # Migration runner
│       │   ├── migrations/     # SQL migrations
│       │   ├── *_repository.go # Repository interfaces
│       │   └── *.go            # Repository implementations
│       ├── handlers/           # HTTP handlers
│       │   ├── handlers.go     # Handler struct & DI
│       │   └── *.go            # Domain-specific handlers
│       ├── middleware/         # HTTP middleware
│       │   └── clerk.go        # Clerk JWT validation
│       ├── models/             # Domain models
│       │   └── *.go            # Entity definitions
│       ├── services/           # Business logic
│       │   └── *.go            # Domain services
│       └── storage/            # S3-compatible storage (optional)
├── frontend/
│   ├── src/
│   │   ├── main.tsx            # App entry & routing
│   │   ├── api/generated/      # Orval-generated API client
│   │   ├── components/         # Reusable UI components
│   │   │   ├── auth/           # Auth components
│   │   │   ├── guards/         # Route protection
│   │   │   └── ui/             # shadcn/ui components
│   │   ├── contexts/           # React contexts
│   │   ├── hooks/              # Custom hooks
│   │   ├── pages/              # Page components
│   │   │   ├── main/           # Public storefront
│   │   │   ├── dashboard/      # User dashboard
│   │   │   └── goose/          # Admin panel
│   │   ├── providers/          # Context providers
│   │   └── services/           # API client setup
│   └── orval.config.ts         # API client generation config
├── docs/                       # Documentation
├── scripts/                    # Utility scripts
├── docker-compose.yml          # Development infrastructure
└── Makefile                    # Build commands

Architecture Layers

1. API Layer (OpenAPI-First)

All endpoints are defined in api/openapi.yaml. The workflow:

  1. Define endpoint in OpenAPI spec
  2. Run make generate-schema → generates Go types and router interface
  3. Implement handler methods matching the interface
  4. Run make generate-frontend → generates TypeScript API client
# api/paths/products.yaml
/products:
  get:
    operationId: listProducts
    parameters:
      - name: limit
        in: query
        schema:
          type: integer

2. Handlers (HTTP Layer)

Thin HTTP handlers that delegate to services. Located in internal/handlers/.

// handlers/handlers.go - Central struct with all dependencies
type Handler struct {
    productService     ProductService
    transactionService TransactionService
    userService        UserService
    logger             *slog.Logger
}

// handlers/product.go - Domain-specific handlers
func (h *Handler) ListProducts(w http.ResponseWriter, r *http.Request, params api.ListProductsParams) {
    products, err := h.productService.ListActiveProducts(r.Context(), limit, offset)
    // ... handle response
}

Key principle: Handlers only handle HTTP concerns (parsing, validation, responses). Business logic lives in services.

3. Services (Business Logic)

Contains business rules and orchestrates repository calls. Located in internal/services/.

// services/product.go
type ProductService struct {
    productRepo db.ProductRepository
    orgRepo     db.OrgRepository
}

func (s *ProductService) CreateProduct(ctx context.Context, req *models.CreateProductRequest) (*models.Product, error) {
    // Validate subscription days for membership type
    if req.Type == models.ProductTypeMembership && req.SubscriptionDays == nil {
        return nil, fmt.Errorf("subscription_days is required")
    }
    // ... create product via repository
}

4. Database Layer (Repositories)

Repository pattern with interfaces for testability. Located in internal/db/.

// db/product_repository.go - Interface
type ProductRepository interface {
    Create(ctx context.Context, product *models.Product) error
    GetByID(ctx context.Context, id uuid.UUID) (*models.Product, error)
    List(ctx context.Context, limit, offset int) ([]*models.Product, error)
}

// db/product.go - Implementation
type productRepository struct {
    pool *pgxpool.Pool
}

func (r *productRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.Product, error) {
    query := `SELECT id, org_id, name, ... FROM products WHERE id = $1`
    // ... execute query
}

Pattern: Each domain has *_repository.go (interface) and *.go (implementation).

5. Models (Domain Types)

Pure data structures representing domain entities. Located in internal/models/.

// models/product.go
type Product struct {
    ID               uuid.UUID   `json:"id"`
    OrgID            uuid.UUID   `json:"org_id"`
    Name             string      `json:"name"`
    Type             ProductType `json:"type"`
    Price            int64       `json:"price"`  // in cents
    SubscriptionDays *int        `json:"subscription_days,omitempty"`
}

Dependency Injection

All dependencies are wired in main.go:

func main() {
    // 1. Load config
    cfg, _ := config.Load()

    // 2. Initialize database client (aggregates all repositories)
    dbClient, _ := db.NewClient(ctx, cfg.GetDatabaseURL())

    // 3. Initialize services (inject repositories)
    userService := services.NewUserService(dbClient.Users, dbClient.Orgs)
    productService := services.NewProductService(dbClient.Products, dbClient.Orgs)

    // 4. Initialize handlers (inject services)
    handler := handlers.NewHandler(productService, transactionService, userService, logger)

    // 5. Create router with handlers
    router := api.NewRouter(api.RouterConfig{Handler: handler, ...})
}

Request Flow

HTTP Request
[Middleware Stack]
  - RequestID
  - RealIP
  - Logger
  - Recoverer
  - CORS
  - Clerk Auth (JWT validation)
[Router] (Chi + oapi-codegen generated)
[Handler] - Parse request, call service, send response
[Service] - Business logic, validation, orchestration
[Repository] - Database operations
[PostgreSQL]

Authentication Flow

Development Mode (No Clerk)

  • DefaultAuthMiddleware injects fixed user/org IDs
  • No real authentication

Production Mode (Clerk)

  • OptionalClerkAuthMiddleware validates JWT from Authorization: Bearer {token}
  • Syncs user to database on first login
  • Injects user ID into request context

Frontend Architecture

Routing

TanStack Router with file-based route organization:

// main.tsx
const routeTree = rootRoute.addChildren([
  mainLayoutRoute.addChildren([   // Public storefront
    indexRoute,                   // /
    productDetailRoute,           // /products/$id
    checkoutRoute,                // /checkout
  ]),
  dashboardLayoutRoute.addChildren([  // User dashboard
    dashboardRoute,               // /dashboard
    purchasesRoute,               // /dashboard/purchases
  ]),
  gooseRoute,                     // /goose (admin)
])

Provider Stack

<ClerkProvider>           {/* Authentication */}
  <QueryProvider>         {/* Data fetching */}
    <CartProvider>        {/* Shopping cart state */}
      <ThemeProvider>     {/* Dark/light mode */}
        <RouterProvider />
      </ThemeProvider>
    </CartProvider>
  </QueryProvider>
</ClerkProvider>

API Client

Generated by Orval from OpenAPI spec:

// Generated: src/api/generated/products/products.ts
export const useListProducts = (params?: ListProductsParams) => {
  return useQuery({
    queryKey: ['products', params],
    queryFn: () => apiClient.get('/products', { params }),
  })
}

Database Migrations

Auto-run on backend startup. Located in internal/db/migrations/.

001_initial_schema.sql      # Base tables
002_seed_data.sql           # Initial data
003_marketplace_schema.sql  # Products, transactions
004_seed_marketplace_data.sql
005_system_admins.sql       # Admin roles
006_dev_dummy_data.sql      # Development data
007_add_clerk_id.sql        # Clerk integration

Rules: - Migrations are idempotent (use IF NOT EXISTS) - Never modify applied migrations - Migrations run sequentially by filename order

Domain Separation Pattern

Each domain follows the same file structure across all layers:

Domain Models DB Repository DB Impl Service Handler
Product product.go product_repository.go product.go product.go product.go
Transaction transaction.go transaction_repository.go transaction.go transaction.go transaction.go
User user.go user_repository.go user.go user.go user.go
Org org.go org_repository.go org.go org.go org.go
Asset asset.go asset_repository.go asset.go asset.go asset.go
License license.go license_repository.go license.go (via org) (via org)
Entitlement entitlement.go entitlement_repository.go entitlement.go entitlement.go (via asset)

Common Commands

make dev              # Start all services (Docker)
make dev-frontend     # Start frontend dev server
make test-backend     # Run backend tests
make generate-schema  # Generate Go code from OpenAPI
make generate-frontend # Generate frontend API client
make lint             # Run linters
make clean            # Clean build artifacts

Environment Variables

Key configuration (see DEPLOYMENT.md for full list):

Variable Description
DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME PostgreSQL connection
SERVER_HOST, SERVER_PORT Backend server
CLERK_SECRET_KEY Clerk authentication
VITE_CLERK_PUBLISHABLE_KEY Frontend Clerk key
VITE_API_URL Backend API URL

Testing Strategy

  • Unit tests: Services and handlers with mocked dependencies
  • Mocking: Use testify/mock with interfaces
  • Coverage target: >70% for new code
  • Location: Tests alongside code (*_test.go)
func TestProductService_CreateProduct(t *testing.T) {
    mockRepo := new(MockProductRepository)
    service := NewProductService(mockRepo, mockOrgRepo)

    mockRepo.On("Create", mock.Anything, mock.Anything).Return(nil)

    product, err := service.CreateProduct(ctx, req)

    assert.NoError(t, err)
    mockRepo.AssertExpectations(t)
}