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:
- Define endpoint in OpenAPI spec
- Run
make generate-schema→ generates Go types and router interface - Implement handler methods matching the interface
- 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)¶
DefaultAuthMiddlewareinjects fixed user/org IDs- No real authentication
Production Mode (Clerk)¶
OptionalClerkAuthMiddlewarevalidates JWT fromAuthorization: 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/mockwith 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)
}