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
-
Define the API
// proto/tenki/cloud/workspace/v1/project.proto service ProjectService { rpc CreateProject(CreateProjectRequest) returns (CreateProjectResponse); } -
Generate code
bufgen -
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 } -
Write SQL queries
-- queries/workspace/project.sql -- name: CreateProject :one INSERT INTO projects (name, workspace_id) VALUES ($1, $2) RETURNING *; -
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: ¶ms.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
- Write query in
backend/queries/ - Run
sqlc generate - 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
- Use prepared statements - sqlc does this automatically
- Batch operations - Use
CopyFromfor bulk inserts - Connection pooling - Configure in engine.yaml
- Context cancellation - Always respect context.Done()
- 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))
})
}