Overview
Prompts.dev uses a modular monolith architecture. The codebase is organized into focused internal modules with clear boundaries, but deployed as a single application.
System Components
The system consists of three main components:
┌─────────┐ ┌──────────────┐ ┌─────────────┐
│ CLI │ ────────>│ API Server │ ────────>│ PostgreSQL │
└─────────┘ HTTP └──────────────┘ SQL └─────────────┘
│
│ S3 API
v
┌─────────────┐
│ Cloudflare │
│ R2 │
└─────────────┘
CLI (cmd/cli) - Command-line interface for developers
API Server (cmd/api) - HTTP REST API built with Gin
PostgreSQL - Relational database for metadata storage
Cloudflare R2 - Object storage for prompt tarballs (S3-compatible)
Internal Modules
The internal/ directory contains focused modules, each with specific responsibilities:
Core Business Logic
auth
users
identities
prompts
versions
// internal/auth
// OAuth login/callback flows
// JWT token issuance and validation
// Authentication middleware
Infrastructure
// internal/storage
// Cloudflare R2 client abstraction
// Presigned URL generation
// S3-compatible interface
Request Lifecycle
Every API request flows through these stages:
Request enters Gin router
All routes are mounted under /v1 for versioning: v1 := r . Group ( "/v1" )
v1 . GET ( "/prompts" , promptsHandler . Search )
v1 . POST ( "/prompts" , authMiddleware , promptsHandler . Create )
Request ID middleware
Assigns a unique UUID to each request and sets X-Request-ID header: func requestIDMiddleware () gin . HandlerFunc {
return func ( c * gin . Context ) {
id := uuid . NewString ()
c . Set ( requestIDKey , id )
c . Writer . Header (). Set ( "X-Request-ID" , id )
c . Next ()
}
}
Authentication middleware (protected routes only)
Validates JWT bearer token on protected routes: authorized := v1 . Group ( "/" )
authorized . Use ( authMiddleware . Authenticate ())
{
authorized . POST ( "/prompts" , promptsHandler . Create )
authorized . POST ( "/prompts/:id/versions" , versionsHandler . Upload )
}
Handler executes business logic
Handler calls repository methods to interact with the database: func ( h * Handler ) Create ( c * gin . Context ) {
// Validate request
// Extract user ID from context
// Call repository
p , err := h . repo . Create ( ctx , prompt , tags )
// Return response
}
Structured JSON response
Responses use a consistent envelope format: {
"request_id" : "550e8400-e29b-41d4-a716-446655440000" ,
"data" : { ... }
}
Request logging
Structured JSON logs include status code, latency, and metadata: {
"request_id" : "550e8400-e29b-41d4-a716-446655440000" ,
"method" : "POST" ,
"path" : "/v1/prompts" ,
"status" : 201 ,
"latency_ms" : 45 ,
"user_id" : "user_123"
}
Layer Architecture
The codebase follows a layered architecture pattern:
┌─────────────────────────────────────┐
│ API Handlers (HTTP Layer) │ ← Gin handlers, request/response
├─────────────────────────────────────┤
│ Business Logic (Service Layer) │ ← Services, domain logic
├─────────────────────────────────────┤
│ Data Access (Repository Layer) │ ← GORM repositories
├─────────────────────────────────────┤
│ Database (PostgreSQL) │ ← Persistent storage
└─────────────────────────────────────┘
Example: Prompts Module
handler.go
repository.go
model.go
// HTTP layer - handles requests and responses
type Handler struct {
repo Repository
}
func ( h * Handler ) Create ( c * gin . Context ) {
var req createPromptRequest
c . ShouldBindJSON ( & req )
p , err := h . repo . Create ( ctx , prompt , tags )
server . RespondJSON ( c , http . StatusCreated , p )
}
Authentication Architecture
Prompts.dev uses provider-agnostic OAuth with JWT tokens:
OAuth Login Initiation
Client requests login for a provider: GET /v1/auth/{provider}/login
# Supported: github, google
Server redirects to OAuth provider’s authorization page.
OAuth Callback
Provider redirects back with authorization code: GET /v1/auth/{provider}/callback?code=...
Server exchanges code for access token and fetches user profile.
User Creation or Linking
New user : Creates entry in users table
Existing identity : Links to existing user via user_identities
Auto-linking : Only enabled when provider email is verified
JWT Token Issuance
Server issues a signed JWT token: {
"token" : "eyJhbGciOiJIUzI1NiIs..." ,
"user" : {
"id" : "user_123" ,
"username" : "alice" ,
"email" : "[email protected] "
}
}
Database Schema
-- Users table
CREATE TABLE users (
id UUID PRIMARY KEY ,
username VARCHAR UNIQUE ,
email VARCHAR ,
created_at TIMESTAMP
);
-- Provider identities (OAuth linking)
CREATE TABLE user_identities (
id UUID PRIMARY KEY ,
user_id UUID REFERENCES users(id),
provider VARCHAR , -- 'github' or 'google'
provider_id VARCHAR ,
UNIQUE ( provider , provider_id)
);
Storage Architecture
Prompt tarballs are stored in Cloudflare R2 (S3-compatible object storage):
Upload Flow
Client uploads tarball
POST /v1/prompts/:id/versions
Content-Type: multipart/form-data
API uploads to R2
key := fmt . Sprintf ( " %s / %s / %s .tar.gz" , owner , name , version )
storageClient . Upload ( ctx , key , file , size , "application/gzip" )
Metadata stored in database
INSERT INTO prompt_versions (id, prompt_id, version , tarball_url)
VALUES ( '...' , '...' , '1.0.0' , 's3://bucket/owner/name/1.0.0.tar.gz' );
Download Flow
Client requests download
GET /v1/prompts/:owner/:name/versions/:version/download
API generates presigned URL
url , err := storageClient . GetPresignedURL ( ctx , key , 15 * time . Minute )
API redirects to presigned URL
HTTP 302 Found
Location: https://bucket.r2.cloudflarestorage.com/...?signature=...
Client downloads from R2 directly
Download happens directly from R2, reducing API load.
Presigned URLs expire after 15 minutes for security.
Module Boundaries
The architecture enforces clear boundaries:
Dependency Rules
cmd/api composes concrete implementations
// cmd/api/main.go
usersRepo := users . NewGORMRepository ( gdb )
authService := auth . NewAuthService ( cfg , usersRepo , identitiesRepo )
authHandler := auth . NewHandler ( authService , cfg )
Modules interact through interfaces
// Handler depends on interface, not concrete implementation
type Handler struct {
repo Repository // interface
}
Shared response format in server package
// All modules use common response helpers
server . RespondJSON ( c , status , data )
server . RespondError ( c , status , code , message )
server . RespondValidationError ( c , fieldErrors )
Modules should NOT import each other’s concrete implementations. Use interfaces for dependencies.
Error Handling
All API errors use a standard envelope format:
{
"request_id" : "550e8400-e29b-41d4-a716-446655440000" ,
"error" : {
"code" : "VALIDATION_ERROR" ,
"message" : "Invalid request parameters" ,
"fields" : {
"name" : "must match ^[a-z0-9-]+$"
}
}
}
Common error codes:
VALIDATION_ERROR - Invalid request data
UNAUTHORIZED - Missing or invalid authentication
NOT_FOUND - Resource not found
CONFLICT - Resource already exists
INTERNAL_ERROR - Server error
Middleware Stack
The API server uses these middleware in order:
r . Use ( requestIDMiddleware ()) // 1. Assign request ID
r . Use ( recoveryMiddleware ()) // 2. Panic recovery
r . Use ( loggerMiddleware ( logger )) // 3. Structured logging
r . Use ( corsMiddleware ()) // 4. CORS headers
Protected routes add authentication:
authorized . Use ( authMiddleware . Authenticate ())
Database Migrations
Migrations are SQL files in the migrations/ directory:
-- 001_create_users.up.sql
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR ( 255 ) UNIQUE NOT NULL ,
email VARCHAR ( 255 ),
created_at TIMESTAMP DEFAULT NOW ()
);
Migrations run automatically on API startup using the migration runner in internal/db.
Migrations should be idempotent - safe to run multiple times without side effects.