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 docsCode 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 operationsprojects.go- Project CRUD and sharingllm_services.go- LLM service/instance managementembeddings.go- Embedding CRUD operationssimilars.go- Similarity searchadmin.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: falseRegenerate code after query changes:
sqlc generate --no-remote5. 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#
- Automatic OpenAPI generation - No manual spec maintenance
- Request/response validation - Type-safe with JSON schema
- Error handling - Standardized error responses
- 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 compilerBenefits:
- 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.MinuteQuery 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.goDocker 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:
- Command line flags:
--port 8080 - Environment variables:
SERVICE_PORT=8080 .envfile:SERVICE_PORT=8080
Priority: CLI flags > Environment > .env file > Defaults
Next Steps#
- Testing Guide - Learn how to test changes
- Contributing Guide - Start contributing
- Performance Guide - Optimize queries and indexes