Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Backend Development Guide

Last updated: 2025-06-12

Overview

The Tenki backend is built with Go and follows Domain-Driven Design principles. Services communicate via Connect RPC (gRPC-Web compatible) and use Temporal for workflow orchestration.

Project Structure

backend/
├── cmd/                  # Application entry points
│   ├── engine/           # Main backend service
│   └── tenki-cli/        # CLI tool
├── internal/             # Private application code
│   ├── app/              # Application layer
│   └── domain/           # Business domains
│       ├── billing/      # Billing domain
│       ├── compute/      # VM management
│       ├── identity/     # Auth & users
│       ├── runner/       # GitHub runners
│       └── workspace/    # Multi-tenancy
├── pkg/                  # Public packages
│   ├── proto/            # Generated protobuf
├── queries/              # SQL queries (sqlc)
└── schema/               # Database migrations

Development Workflow

Running the Backend

# Start dependencies
dev up postgres temporal kafka

# Run migrations
db deploy

# Start engine
cd backend
go run cmd/engine/main.go

# Or use the dev script
dev restart engine

Adding a New Feature

  1. Define the API

    // proto/tenki/cloud/workspace/v1/project.proto
    service ProjectService {
      rpc CreateProject(CreateProjectRequest) returns (CreateProjectResponse);
    }
    
  2. Generate code

    bufgen
    
  3. Implement domain logic

    // internal/domain/workspace/service/project.go
    func (s *Service) CreateProject(ctx context.Context, req *params.CreateProject) (*models.Project, error) {
        // Business logic here
    }
    
  4. Write SQL queries

    -- queries/workspace/project.sql
    -- name: CreateProject :one
    INSERT INTO projects (name, workspace_id)
    VALUES ($1, $2)
    RETURNING *;
    
  5. Generate SQL code

    cd backend && sqlc generate
    

Testing

Unit Tests

func TestService_CreateProject(t *testing.T) {
    tests := []struct {
        name    string
        input   *params.CreateProject
        want    *models.Project
        wantErr bool
    }{
        {
            name: "valid project",
            input: &params.CreateProject{
                Name:        "test-project",
                WorkspaceID: "ws-123",
            },
            want: &models.Project{
                Name: "test-project",
            },
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // Test implementation
        })
    }
}

Integration Tests

//go:build integration

var _ = Describe("Project Service", func() {
    var (
        service *workspace.Service
        db      *sql.DB
    )

    BeforeEach(func() {
        db = setupTestDB()
        service = workspace.NewService(workspace.WithDB(db))
    })

    It("should create a project", func() {
        project, err := service.CreateProject(ctx, params)
        Expect(err).NotTo(HaveOccurred())
        Expect(project.Name).To(Equal("test"))
    })
})

Running Tests

# Unit tests only
gotest

# Integration tests
gotest-integration

# Specific package
cd backend && go test ./internal/domain/workspace/...

# With coverage
cd backend && go test -cover ./...

Database Operations

Migrations

# Create migration
echo "CREATE TABLE features (id uuid PRIMARY KEY);" > backend/schema/$(date +%Y%m%d%H%M%S)_add_features.sql

# Apply migrations
db up

# Rollback
db down

Query Development

  1. Write query in backend/queries/
  2. Run sqlc generate
  3. Use generated code in service
// Generated code usage
project, err := s.db.CreateProject(ctx, db.CreateProjectParams{
    Name:        req.Name,
    WorkspaceID: req.WorkspaceID,
})

Temporal Workflows

Workflow Definition

func RunnerProvisioningWorkflow(ctx workflow.Context, params RunnerParams) error {
    // Configure workflow
    ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
        StartToCloseTimeout: 10 * time.Minute,
        RetryPolicy: &temporal.RetryPolicy{
            MaximumAttempts: 3,
        },
    })

    // Execute activities
    var runner *models.Runner
    err := workflow.ExecuteActivity(ctx, CreateRunnerActivity, params).Get(ctx, &runner)
    if err != nil {
        return fmt.Errorf("create runner: %w", err)
    }

    return nil
}

Testing Workflows

func TestRunnerProvisioningWorkflow(t *testing.T) {
    suite := testsuite.WorkflowTestSuite{}
    env := suite.NewTestWorkflowEnvironment()

    // Mock activities
    env.OnActivity(CreateRunnerActivity, mock.Anything).Return(&models.Runner{ID: "123"}, nil)

    // Execute workflow
    env.ExecuteWorkflow(RunnerProvisioningWorkflow, params)

    require.True(t, env.IsWorkflowCompleted())
    require.NoError(t, env.GetWorkflowError())
}

API Patterns

Service Options

// Use functional options pattern
type Service struct {
    db       *db.Queries
    temporal client.Client
    logger   *slog.Logger
}

type Option func(*Service)

func WithDB(db *db.Queries) Option {
    return func(s *Service) {
        s.db = db
    }
}

func NewService(opts ...Option) *Service {
    s := &Service{
        logger: slog.Default(),
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

Error Handling

// Define domain errors
var (
    ErrProjectNotFound = errors.New("project not found")
    ErrUnauthorized    = errors.New("unauthorized")
)

// Wrap errors with context
if err != nil {
    return fmt.Errorf("fetch project %s: %w", projectID, err)
}

// Check errors
if errors.Is(err, ErrProjectNotFound) {
    return connect.NewError(connect.CodeNotFound, err)
}

Debugging

Local Debugging

# Enable debug logging
export LOG_LEVEL=debug

# Run with delve
dlv debug cmd/engine/main.go

# Attach to running process
dlv attach $(pgrep engine)

Temporal UI

# View workflows
open https://temporal.tenki.lab

# List workflows via CLI
temporal workflow list --query 'WorkflowType="RunnerProvisioningWorkflow"'

# Describe workflow
temporal workflow describe -w <workflow-id>

Database Queries

# Connect to database
dev exec postgres psql -U postgres tenki

# Useful queries
SELECT * FROM runners WHERE created_at > NOW() - INTERVAL '1 hour';
SELECT COUNT(*) FROM workflow_runs GROUP BY status;

Performance Tips

  1. Use prepared statements - sqlc does this automatically
  2. Batch operations - Use CopyFrom for bulk inserts
  3. Connection pooling - Configure in engine.yaml
  4. Context cancellation - Always respect context.Done()
  5. Concurrent operations - Use errgroup for parallel work

Common Patterns

Repository Pattern

type RunnerRepository interface {
    Create(ctx context.Context, runner *Runner) error
    GetByID(ctx context.Context, id string) (*Runner, error)
    List(ctx context.Context, filter Filter) ([]*Runner, error)
}

Builder Pattern

query := NewQueryBuilder().
    Where("status", "active").
    OrderBy("created_at", "DESC").
    Limit(10).
    Build()

Middleware Pattern

func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        slog.Info("request", "method", r.Method, "path", r.URL.Path, "duration", time.Since(start))
    })
}

Resources