Technical Architecture#

This document provides a technical deep-dive into embapi’s architecture for developers who want to understand or modify the codebase.

Project Structure#

embapi/
├── main.go                        # Application entry point
├── go.mod                         # Go module definition
├── go.sum                         # Dependency checksums
├── sqlc.yaml                      # sqlc configuration
├── template.env                   # Environment template
├── .env                           # Local config (gitignored)
│
├── api/
│   └── openapi.yml                # OpenAPI spec (not actively maintained)
│
├── internal/                      # Internal packages (non-importable)
│   ├── auth/                      # Authentication logic
│   │   └── authenticate.go        # Bearer token validation
│   │
│   ├── crypto/                    # Encryption utilities
│   │   └── crypto.go              # AES-256-GCM encryption
│   │
│   ├── database/                  # Database layer
│   │   ├── database.go            # Connection pool management
│   │   ├── migrations.go          # Migration runner
│   │   ├── db.go                  # Generated by sqlc
│   │   ├── models.go              # Generated by sqlc
│   │   ├── queries.sql.go         # Generated by sqlc
│   │   │
│   │   ├── migrations/            # SQL migrations
│   │   │   ├── 001_create_initial_scheme.sql
│   │   │   ├── 002_create_emb_index.sql
│   │   │   ├── 003_add_public_read_flag.sql
│   │   │   ├── 004_refactor_llm_services_architecture.sql
│   │   │   ├── tern.conf.tpl      # Template for tern
│   │   │   └── tern.conf          # Generated (gitignored)
│   │   │
│   │   └── queries/               # SQL queries for sqlc
│   │       └── queries.sql        # All database queries
│   │
│   ├── handlers/                  # HTTP request handlers
│   │   ├── handlers.go            # Common handler utilities
│   │   ├── users.go               # User endpoints
│   │   ├── projects.go            # Project endpoints
│   │   ├── llm_services.go        # LLM service endpoints
│   │   ├── api_standards.go       # API standard endpoints
│   │   ├── embeddings.go          # Embedding endpoints
│   │   ├── similars.go            # Similarity search endpoints
│   │   ├── admin.go               # Admin endpoints
│   │   │
│   │   └── *_test.go              # Test files
│   │       ├── users_test.go
│   │       ├── projects_test.go
│   │       ├── projects_sharing_test.go
│   │       ├── embeddings_test.go
│   │       ├── llm_services_test.go
│   │       ├── editor_permissions_test.go
│   │       └── handlers_test.go
│   │
│   └── models/                    # Data models and options
│       ├── options.go             # CLI/environment options
│       ├── users.go               # User models
│       ├── projects.go            # Project models
│       ├── instances.go           # LLM instance models (new)
│       ├── api_standards.go       # API standard models
│       ├── embeddings.go          # Embedding models
│       ├── similars.go            # Similarity search models
│       └── admin.go               # Admin operation models
│
├── testdata/                      # Test fixtures
│   ├── postgres/                  # PostgreSQL test data
│   │   ├── enable-vector.sql
│   │   └── users.yml
│   │
│   └── *.json                     # JSON test fixtures
│       ├── valid_user.json
│       ├── valid_embeddings.json
│       ├── valid_api_standard_*.json
│       └── valid_llm_service_*.json
│
└── docs/                          # Documentation
    ├── content/                   # Hugo content
    └── *.md                       # Additional docs

Code Organization#

1. Entry Point (main.go)#

The application entry point handles:

func main() {
	// 1. Parse CLI options and environment variables
	cli := huma.NewCLI(func(hooks huma.Hooks, opts *models.Options) {
		
		// 2. Initialize database connection pool
		pool := database.InitDB(opts)
		defer pool.Close()
		
		// 3. Run database migrations
		database.RunMigrations(pool, opts)
		
		// 4. Create HTTP router and Huma API
		router := http.NewServeMux()
		api := humago.New(router, huma.DefaultConfig("embapi", "0.1.0"))
		
		// 5. Register all routes
		handlers.AddRoutes(pool, keyGen, api)
		
		// 6. Start HTTP server
		server := &http.Server{
			Addr:    fmt.Sprintf("%s:%d", opts.Host, opts.Port),
			Handler: router,
		}
		server.ListenAndServe()
	})
	
	cli.Run()
}

2. Handlers (internal/handlers/)#

Handlers process HTTP requests using the Huma framework pattern:

// Example: Create User Handler
func RegisterUsersRoutes(pool *pgxpool.Pool, keyGen RandomKeyGenerator, api huma.API) {
	
	// Define request/response types
	type CreateUserInput struct {
		Body models.CreateUserRequest
	}
	
	type CreateUserOutput struct {
		Body models.User
	}
	
	// Register operation with Huma
	huma.Register(api, huma.Operation{
		OperationID: "create-user",
		Method:      http.MethodPost,
		Path:        "/v1/users",
		Summary:     "Create a new user",
		Description: "Admin only. Creates user and returns API key.",
		Security:    []map[string][]string{{"bearer": {}}},
	}, func(ctx context.Context, input *CreateUserInput) (*CreateUserOutput, error) {
		
		// 1. Get authenticated user from context
		authUser := auth.GetAuthenticatedUser(ctx)
		
		// 2. Check authorization (admin only)
		if !authUser.IsAdmin {
			return nil, huma.Error403Forbidden("admin access required")
		}
		
		// 3. Validate input
		if err := input.Body.Validate(); err != nil {
			return nil, huma.Error400BadRequest("invalid input", err)
		}
		
		// 4. Business logic
		user, apiKey, err := createUserWithKey(ctx, pool, keyGen, &input.Body)
		if err != nil {
			return nil, handleDatabaseError(err)
		}
		
		// 5. Return response
		return &CreateUserOutput{Body: *user}, nil
	})
}

Handler file organization:

  • handlers.go - Common utilities (context keys, error handling)
  • users.go - User CRUD operations
  • projects.go - Project CRUD and sharing
  • llm_services.go - LLM service/instance management
  • embeddings.go - Embedding CRUD operations
  • similars.go - Similarity search
  • admin.go - Administrative operations

3. Models (internal/models/)#

Models define request/response structures:

// User model
type User struct {
	UserHandle string    `json:"user_handle" example:"alice"`
	Name       string    `json:"name" example:"Alice Smith"`
	Email      string    `json:"email" example:"alice@example.com"`
	CreatedAt  time.Time `json:"created_at"`
	UpdatedAt  time.Time `json:"updated_at"`
}

// Request model with validation
type CreateUserRequest struct {
	UserHandle string `json:"user_handle" minLength:"1" maxLength:"50" pattern:"^[a-z0-9_-]+$"`
	Name       string `json:"name" minLength:"1" maxLength:"100"`
	Email      string `json:"email" format:"email"`
}

func (r *CreateUserRequest) Validate() error {
	if r.UserHandle == "" {
		return fmt.Errorf("user_handle is required")
	}
	if r.UserHandle == "_system" {
		return fmt.Errorf("_system is a reserved handle")
	}
	return nil
}

Model conventions:

  • Request models: Create*Request, Update*Request
  • Response models: *Response, *Output
  • Database models: Match database schema (generated by sqlc)

4. Database Layer (internal/database/)#

Connection Management (database.go)#

func InitDB(opts *models.Options) *pgxpool.Pool {
	connString := fmt.Sprintf(
		"postgres://%s:%s@%s:%d/%s",
		opts.DBUser, opts.DBPassword, 
		opts.DBHost, opts.DBPort, opts.DBName,
	)
	
	config, err := pgxpool.ParseConfig(connString)
	if err != nil {
		log.Fatal(err)
	}
	
	// Configure connection pool
	config.MaxConns = 20
	config.MinConns = 5
	config.MaxConnIdleTime = time.Minute * 5
	
	pool, err := pgxpool.NewWithConfig(context.Background(), config)
	if err != nil {
		log.Fatal(err)
	}
	
	return pool
}

Migrations (migrations.go)#

Uses tern for versioned migrations:

func RunMigrations(pool *pgxpool.Pool, opts *models.Options) error {
	// Create tern configuration
	config := createTernConfig(opts)
	
	// Initialize migrator
	migrator, err := migrate.NewMigrator(context.Background(), pool, "schema_version")
	if err != nil {
		return err
	}
	
	// Run pending migrations
	err = migrator.Migrate(context.Background())
	if err != nil {
		return fmt.Errorf("migration failed: %w", err)
	}
	
	return nil
}

Migration files are numbered sequentially:

-- 001_create_initial_scheme.sql
CREATE TABLE users (
    user_id SERIAL PRIMARY KEY,
    user_handle TEXT UNIQUE NOT NULL,
    embapi_key TEXT UNIQUE NOT NULL,
    ...
);

CREATE TABLE projects (
    project_id SERIAL PRIMARY KEY,
    project_handle TEXT NOT NULL,
    owner TEXT NOT NULL REFERENCES users(user_handle),
    ...
);

-- 002_create_emb_index.sql  
CREATE INDEX embedding_vector_idx 
ON embeddings 
USING hnsw (vector vector_cosine_ops);

SQLC Queries (queries/queries.sql)#

Write SQL, generate type-safe Go code:

-- name: UpsertUser :one
INSERT INTO users (
  user_handle, name, email, embapi_key, created_at, updated_at
) VALUES (
  $1, $2, $3, $4, NOW(), NOW()
)
ON CONFLICT (user_handle) DO UPDATE SET
  name = EXCLUDED.name,
  email = EXCLUDED.email,
  updated_at = NOW()
RETURNING *;

-- name: GetUserByHandle :one
SELECT * FROM users WHERE user_handle = $1;

-- name: GetAllUsers :many
SELECT user_handle, name, email, created_at, updated_at 
FROM users 
ORDER BY user_handle ASC;

-- name: DeleteUser :exec
DELETE FROM users WHERE user_handle = $1;

Generated Go code (queries.sql.go):

// Generated by sqlc
func (q *Queries) UpsertUser(ctx context.Context, arg UpsertUserParams) (User, error) {
	row := q.db.QueryRow(ctx, upsertUser,
		arg.UserHandle,
		arg.Name,
		arg.Email,
		arg.VdbKey,
	)
	var i User
	err := row.Scan(
		&i.UserID,
		&i.UserHandle,
		&i.Name,
		&i.Email,
		&i.VdbKey,
		&i.CreatedAt,
		&i.UpdatedAt,
	)
	return i, err
}

sqlc configuration (sqlc.yaml):

version: "2"
sql:
  - engine: "postgresql"
    queries: "internal/database/queries/queries.sql"
    schema: "internal/database/migrations/"
    gen:
      go:
        package: "database"
        out: "internal/database"
        emit_json_tags: true
        emit_db_tags: true
        emit_prepared_queries: false
        emit_interface: false

Regenerate code after query changes:

sqlc generate --no-remote

5. Authentication (internal/auth/)#

Token-based authentication using Bearer tokens:

// Middleware checks Authorization header
func AuthMiddleware(pool *pgxpool.Pool, adminKey string) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			
			// Extract Bearer token
			authHeader := r.Header.Get("Authorization")
			token := strings.TrimPrefix(authHeader, "Bearer ")
			
			if token == "" {
				// Allow public access for certain endpoints
				ctx := context.WithValue(r.Context(), "user", nil)
				next.ServeHTTP(w, r.WithContext(ctx))
				return
			}
			
			// Check admin key
			if token == adminKey {
				user := &AuthUser{Handle: "_admin", IsAdmin: true}
				ctx := context.WithValue(r.Context(), "user", user)
				next.ServeHTTP(w, r.WithContext(ctx))
				return
			}
			
			// Look up user by API key hash
			hash := hashAPIKey(token)
			user, err := db.GetUserByKeyHash(r.Context(), pool, hash)
			if err != nil {
				http.Error(w, "Unauthorized", http.StatusUnauthorized)
				return
			}
			
			authUser := &AuthUser{Handle: user.UserHandle, IsAdmin: false}
			ctx := context.WithValue(r.Context(), "user", authUser)
			next.ServeHTTP(w, r.WithContext(ctx))
		})
	}
}

// Helper to get authenticated user from context
func GetAuthenticatedUser(ctx context.Context) *AuthUser {
	user, _ := ctx.Value("user").(*AuthUser)
	return user
}

6. Encryption (internal/crypto/)#

AES-256-GCM encryption for sensitive data (API keys):

// Encrypt data with AES-256-GCM
func Encrypt(plaintext []byte, key []byte) ([]byte, error) {
	// Ensure key is 32 bytes
	keyHash := sha256.Sum256(key)
	
	// Create AES cipher
	block, err := aes.NewCipher(keyHash[:])
	if err != nil {
		return nil, err
	}
	
	// Create GCM
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}
	
	// Generate random nonce
	nonce := make([]byte, gcm.NonceSize())
	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		return nil, err
	}
	
	// Encrypt and append nonce
	ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
	return ciphertext, nil
}

func Decrypt(ciphertext []byte, key []byte) ([]byte, error) {
	// Derive key
	keyHash := sha256.Sum256(key)
	
	block, err := aes.NewCipher(keyHash[:])
	if err != nil {
		return nil, err
	}
	
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}
	
	// Extract nonce
	nonceSize := gcm.NonceSize()
	nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
	
	// Decrypt
	plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
	if err != nil {
		return nil, err
	}
	
	return plaintext, nil
}

Huma Framework Integration#

embapi uses Huma for API development:

Benefits#

  1. Automatic OpenAPI generation - No manual spec maintenance
  2. Request/response validation - Type-safe with JSON schema
  3. Error handling - Standardized error responses
  4. Documentation - Interactive docs at /docs

Operation Registration Pattern#

huma.Register(api, huma.Operation{
	OperationID:   "get-project",
	Method:        http.MethodGet,
	Path:          "/v1/projects/{owner}/{project}",
	Summary:       "Get project details",
	Description:   "Returns full project information including metadata schema",
	Tags:          []string{"Projects"},
	Security:      []map[string][]string{{"bearer": {}}},
	MaxBodyBytes:  1024,  // Limit request size
	DefaultStatus: http.StatusOK,
	Errors:        []int{400, 401, 403, 404, 500},
}, handlerFunction)

Input/Output Patterns#

// Path parameters, query parameters, and body
type GetSimilarInput struct {
	Owner     string  `path:"owner" doc:"Project owner"`
	Project   string  `path:"project" doc:"Project handle"`
	TextID    string  `path:"text_id" doc:"Text identifier"`
	Threshold float32 `query:"threshold" default:"0.5" doc:"Similarity threshold"`
	Limit     int     `query:"limit" default:"10" maximum:"200" doc:"Max results"`
}

// Response with status code
type GetSimilarOutput struct {
	Status int
	Body   models.SimilarResponse
}

Error Response Pattern#

// Standard error responses
return nil, huma.Error400BadRequest("validation failed", err)
return nil, huma.Error401Unauthorized("invalid credentials")
return nil, huma.Error403Forbidden("insufficient permissions")
return nil, huma.Error404NotFound("project not found")
return nil, huma.Error500InternalServerError("database error", err)

// Custom error with details
return nil, huma.NewError(400, "Dimension Mismatch", 
	fmt.Sprintf("expected %d dimensions, got %d", expected, actual))

Design Patterns#

1. Repository Pattern (via sqlc)#

Database access is centralized in generated queries:

// Don't write SQL in handlers
// Bad:
rows, err := pool.Query(ctx, "SELECT * FROM users")

// Good: Use generated functions
users, err := db.GetAllUsers(ctx)

2. Dependency Injection#

Pass dependencies explicitly:

// Inject pool into handlers
func RegisterUsersRoutes(pool *pgxpool.Pool, keyGen RandomKeyGenerator, api huma.API) {
	// Routes have access to pool
}

// Store in context for handler access
ctx = context.WithValue(ctx, PoolKey, pool)

3. Interface-Based Testing#

Use interfaces for testability:

// Production: Real random key generator
type StandardKeyGen struct{}
func (s StandardKeyGen) RandomKey(len int) (string, error) {
	b := make([]byte, len)
	_, err := rand.Read(b)
	return hex.EncodeToString(b), err
}

// Testing: Deterministic key generator
type MockKeyGen struct {
	Keys []string
	idx  int
}
func (m *MockKeyGen) RandomKey(len int) (string, error) {
	key := m.Keys[m.idx]
	m.idx++
	return key, nil
}

4. Validation at Multiple Layers#

// 1. Huma validates request structure
type CreateEmbeddingInput struct {
	Body models.Embedding `maxLength:"1000000"`
}

// 2. Model validates business rules
func (e *Embedding) Validate() error {
	if e.TextID == "" {
		return fmt.Errorf("text_id required")
	}
	return nil
}

// 3. Handler validates against database state
func CreateEmbedding(ctx context.Context, input *CreateEmbeddingInput) error {
	// Check project exists
	// Validate dimensions match LLM service
	// Validate metadata against schema
	// Then insert
}

5. Error Wrapping#

Provide context while preserving original error:

user, err := db.GetUserByHandle(ctx, handle)
if err != nil {
	if errors.Is(err, pgx.ErrNoRows) {
		return nil, huma.Error404NotFound("user not found")
	}
	return nil, fmt.Errorf("failed to retrieve user %s: %w", handle, err)
}

Internal Packages#

Go’s internal/ directory enforces package privacy:

// This import works within embapi
import "github.com/mpilhlt/embapi/internal/database"

// This import would FAIL from external projects
// Enforced by Go compiler

Benefits:

  • Clear API boundaries
  • Implementation details hidden
  • Refactoring without breaking external users

Performance Considerations#

Connection Pooling#

config.MaxConns = 20        // Max concurrent connections
config.MinConns = 5         // Keep-alive connections
config.MaxConnIdleTime = 5 * time.Minute

Query Optimization#

Use UNION ALL for better performance:

// Instead of LEFT JOIN with OR conditions
query := `
	SELECT * FROM projects WHERE owner = $1
	UNION ALL
	SELECT p.* FROM projects p
	INNER JOIN projects_shared_with ps USING (project_id)
	WHERE ps.user_handle = $1
	ORDER BY owner, project_handle
`

Index Strategy#

-- Dimension filtering (very important)
CREATE INDEX ON embeddings(project_id, vector_dim);

-- Vector similarity (HNSW for accuracy)
CREATE INDEX ON embeddings USING hnsw (vector vector_cosine_ops);

-- Access lookups
CREATE INDEX ON projects_shared_with(user_handle);
CREATE INDEX ON projects(owner, project_handle);

See Performance Guide for detailed optimization strategies.

Testing Architecture#

Test Organization#

// handlers_test.go - Setup and utilities
func setupTestDB(t *testing.T) *pgxpool.Pool { }
func createTestUser(t *testing.T, pool *pgxpool.Pool) *models.User { }

// users_test.go - User-specific tests
func TestCreateUser(t *testing.T) { }
func TestGetUser(t *testing.T) { }

// projects_sharing_test.go - Sharing feature tests
func TestShareProject(t *testing.T) { }

Testcontainers Integration#

func setupTestDB(t *testing.T) *pgxpool.Pool {
	ctx := context.Background()
	
	// Start PostgreSQL with pgvector
	req := testcontainers.ContainerRequest{
		Image:        "pgvector/pgvector:0.7.4-pg16",
		ExposedPorts: []string{"5432/tcp"},
		Env: map[string]string{
			"POSTGRES_PASSWORD": "password",
			"POSTGRES_DB":       "testdb",
		},
	}
	
	container, err := testcontainers.GenericContainer(ctx, 
		testcontainers.GenericContainerRequest{
			ContainerRequest: req,
			Started:          true,
		})
	require.NoError(t, err)
	
	// Connect and migrate
	pool := connectAndMigrate(ctx, container)
	return pool
}

See Testing Guide for comprehensive testing documentation.

Build and Deployment#

Building#

# Development build
go build -o embapi main.go

# Production build with optimizations
go build -ldflags="-s -w" -o embapi main.go

# Cross-compilation
GOOS=linux GOARCH=amd64 go build -o embapi-linux main.go

Docker Build#

Multi-stage build for minimal image:

# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -ldflags="-s -w" -o embapi main.go

# Runtime stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/embapi .
EXPOSE 8880
CMD ["./embapi"]

Configuration#

Application reads configuration from:

  1. Command line flags: --port 8080
  2. Environment variables: SERVICE_PORT=8080
  3. .env file: SERVICE_PORT=8080

Priority: CLI flags > Environment > .env file > Defaults

Next Steps#