Claude Development Guide for LFX V2 Project Service

May 27, 2026 · View on GitHub

This guide provides essential information for Claude instances working with the LFX V2 Project Service codebase. It includes build commands, architecture patterns, and key technical decisions.

Project Overview

The LFX V2 Project Service is a RESTful API service that manages projects within the Linux Foundation's LFX platform. It provides CRUD operations for projects with built-in authorization and audit capabilities.

Key Technologies

  • Language: Go 1.24+
  • API Framework: Goa v3 (code generation framework)
  • Messaging: NATS with JetStream for event-driven architecture
  • Storage: NATS Key-Value stores (no traditional database)
  • Authentication: JWT with Heimdall middleware
  • Authorization: OpenFGA for fine-grained access control
  • Container: Chainguard distroless images
  • Orchestration: Kubernetes with Helm charts

Architecture Overview

The service follows Clean Architecture principles with clear separation of concerns:

.github/                    # CI/CD workflow files for Github Actions

api/                        # API contracts
└── project/
    └── v1/
        ├── design/         # Goa API design specifications
        └── gen/            # Generated code (gitignored)

charts/                     # Helm charts containing kubernetes template files for deployments

cmd/project-api/            # Presentation Layer (HTTP/NATS entry point)
├── service*.go            # HTTP and NATS handlers
└── main.go                # Application entry point

internal/                   # Core business logic
├── domain/                # Domain layer (interfaces, models, errors)
│   └── models/           # Domain entities
├── service/               # Service layer (business logic)
│   └── email/            # Email template rendering (one file per email type)
└── infrastructure/        # Infrastructure layer
    ├── auth/             # JWT authentication
    ├── log/              # Structured logging helpers (AppendCtx, InitStructureLogConfig)
    ├── middleware/        # HTTP middleware (auth, request ID, body limit, logger)
    └── nats/             # NATS repository and message builder

pkg/                    # Shared packages across services
└── constants/          # Shared constants

scripts/                # Scripts for services and miscellaneous tasks

Key Design Principles

  1. Database Independence: Repository interfaces allow switching storage backends
  2. Testability: Each layer can be tested in isolation using mocks
  3. Event-Driven: All data changes trigger NATS messages for downstream services
  4. Separation of Concerns: Clear boundaries between layers

Development Workflow

Prerequisites

# Install Go 1.23+
# Install Goa framework
go install goa.design/goa/v3/cmd/goa@latest

# Install linting tools
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

Common Development Tasks

1. Generate API Code (REQUIRED after design changes)

make apigen
# or directly: goa gen github.com/linuxfoundation/lfx-v2-project-service/api/project/v1/design -o api/project/v1

2. Build the Service

make build

3. Run Tests

make test              # Run unit tests
make test-verbose      # Verbose output
make test-coverage     # Generate coverage report
make test-integration  # Run integration tests (requires -tags=integration)

4. Run the Service Locally

# Basic run
make run

# With debug logging
make debug

# With custom flags (direct go run)
go run ./cmd/project-api -d -p 8080

5. Lint and Format Code

make fmt    # Format code
make lint   # Run golangci-lint
make check  # Check format and lint without modifying

Code Generation (Goa Framework)

The service uses Goa v3 for API code generation. This is critical to understand:

  1. Design First: API is defined in api/project/v1/design/ files
  2. Generated Code: Running make apigen generates to api/project/v1/gen/:
    • HTTP server/client code
    • Service interfaces
    • OpenAPI specifications
    • Type definitions
  3. Implementation: You implement the generated interfaces in cmd/project-api/service*.go files

Adding New Endpoints

  1. Update api/project/v1/design/project.go with new method
  2. Run make apigen (from repository root) to regenerate code
  3. Implement the new method in cmd/project-api/service_endpoint_project.go
  4. Add tests in cmd/project-api/service_endpoint_project_test.go
  5. Update Heimdall ruleset in charts/*/templates/ruleset.yaml

NATS Messaging Patterns

The service uses NATS for:

  1. Storage: Key-Value stores for project data
  2. Events: Publishing events on data changes
  3. RPC: Handling requests from other services

Key-Value Stores

  • projects: Base project information
  • project-settings: Project settings (separated for access control)

API Endpoints and Message Subjects

Complete API endpoint documentation and NATS message handlers are now documented in README.md.

There are two distinct NATS patterns in this service — both use QueueSubscribe but for different purposes:

Request/reply RPC (internal/service/project_handlers.go): another service sends a request and blocks waiting for a response. The handler calls msg.Respond(data) to return data to the caller.

Event subscriptions (internal/service/project_subscriber.go): the service reacts to events that were already published (including by itself). No caller is waiting — the handler is fire-and-forget and never calls msg.Respond.

// Inbound RPC — request/reply, caller blocks waiting for response
"lfx.projects-api.get_name"            // Get project name by UID
"lfx.projects-api.get_slug"            // Get project slug by UID
"lfx.projects-api.get_logo"            // Get project logo URL by UID
"lfx.projects-api.slug_to_uid"         // Convert slug to UID
"lfx.projects-api.get_parent_uid"      // Get parent project UID

// Inbound events — fire-and-forget, no reply expected
"lfx.projects-api.project_settings.updated" // Sends role notification emails on member additions

// Outbound events (published by this service)
"lfx.index.project"                    // Project created/updated/deleted for indexing
"lfx.index.project_settings"           // Settings created/updated for indexing
"lfx.projects-api.project_settings.updated" // Settings changed (before/after snapshot)
"lfx.fga-sync.update_access"           // Generic FGA access control updates
"lfx.fga-sync.delete_access"           // Generic FGA access control deletion
"lfx.email-service.send_email"         // Request/reply to email service for role notifications

FGA Sync Message Format

The service uses the generic FGA sync handlers for access control. All messages use the GenericFGAMessage envelope:

// Update access control (full sync)
GenericFGAMessage{
    ObjectType: "project",
    Operation: "update_access",
    Data: UpdateAccessData{
        UID: "project-uid",
        Public: true,
        Relations: map[string][]string{
            "writer": []string{"username1", "username2"},
            "auditor": []string{"username3"},
            "meeting_coordinator": []string{"username4"},
        },
        References: map[string][]string{
            "parent": []string{"project:parent-uid"},
        },
    },
}

// Delete all access control
GenericFGAMessage{
    ObjectType: "project",
    Operation: "delete_access",
    Data: DeleteAccessData{
        UID: "project-uid",
    },
}

Key Points:

  • Relations map user roles to usernames (e.g., "writer": ["user1", "user2"])
  • References map object relationships with formatted UIDs (e.g., "parent": ["project:parent-uid"])
  • Update operations are full sync - any relations not included will be removed
  • Delete operations remove all access control tuples for the resource

Testing Patterns

Unit Tests

  • Mock all external dependencies (repository, message builder)
  • Test each layer in isolation
  • Use table-driven tests for comprehensive coverage
  • Write one function tests containing multiple test cases that focus on a single function
  • Focus on testing exported functions of packages
  • Unit tests should be alongside the implementation code with the same file name with a suffix of *_test.go
  • IMPORTANT: Each function should have exactly ONE corresponding test function (e.g., SendIndexProjectTestMessageBuilder_SendIndexProject) which can have multiple tests cases within it.
  • Add test cases within existing test functions if the function you are trying to test already has one rather than creating new test functions

Example Test Structure

func TestEndpoint(t *testing.T) {
    tests := []struct {
        name       string
        payload    *projsvc.Payload
        setupMocks func(*domain.MockRepo, *domain.MockMsg)
        wantErr    bool
    }{
        // Test cases
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            api, mockRepo, mockMsg := setupAPI()
            tt.setupMocks(mockRepo, mockMsg)
            // Test logic
        })
    }
}

Environment Variables

VariableDescriptionDefaultRequired
PORTHTTP listen port8080No
NATS_URLNATS server URLnats://localhost:4222No
LOG_LEVELLog levelinfoNo
JWKS_URLJWT verification endpoint-No
AUDIENCEJWT audiencelfx-v2-project-serviceNo
JWT_AUTH_DISABLED_MOCK_LOCAL_PRINCIPALMock auth for local dev-No
LFX_SELF_SERVE_BASE_URLBase URL for project links in notification emailsderived from LFX_ENVIRONMENTNo
EMAILS_ENABLEDGate for outbound role-notification emails to LFID users (true to enable)falseNo
INVITES_ENABLEDGate for outbound invite requests to non-LFID users (true to enable)falseNo

Authorization (OpenFGA)

When deployed, the service uses OpenFGA for authorization:

  • GET /projects - No check (public list)
  • POST /projects - Requires writer on parent (if specified)
  • GET /projects/:id - Requires viewer on project
  • GET /projects/:id/settings - Requires auditor on project
  • PUT /projects/:id - Requires writer on project
  • PUT /projects/:id/settings - Requires writer on project
  • DELETE /projects/:id - Requires owner on project

Local Development Setup

There are two main development setup options documented in DEVELOPMENT.md:

Option A: Full Platform Setup

For integration testing with complete LFX stack:

  • Install lfx-platform Helm chart (includes NATS, Heimdall, OpenFGA, Authelia, Traefik)
  • Use make helm-install-local with values.local.yaml
  • Full authentication and authorization enabled

Option B: Minimal Setup

For rapid development:

# Just run NATS locally
docker run -d -p 4222:4222 nats:latest -js

# Create KV stores
nats kv add projects --history=20 --storage=file
nats kv add project-settings --history=20 --storage=file

# Run service with mock auth
export NATS_URL=nats://localhost:4222
export JWT_AUTH_DISABLED_MOCK_LOCAL_PRINCIPAL=test-user
make run

Security Note: Option B bypasses all authentication/authorization - only for local development.

New Helm Commands

  • make helm-install-local: Install with local values
  • make helm-restart: Restart deployment pod
  • make docker-build: Build Docker image

Docker Build

# Build from repository root
docker build -t lfx-v2-project-service:latest .

# The Dockerfile uses:
# - Chainguard Go image for building
# - Chainguard static image for runtime (distroless)
# - Multi-stage build for minimal image size

Kubernetes Deployment

# Install Helm chart
helm install lfx-v2-project-service ./charts/lfx-v2-project-service/ -n lfx

# Update deployment
helm upgrade lfx-v2-project-service ./charts/lfx-v2-project-service/ -n lfx

# View generated manifests
helm template lfx-v2-project-service ./charts/lfx-v2-project-service/ -n lfx

Helm Configuration

  • OpenFGA can be disabled for local development
  • NATS KV buckets are created automatically
  • Heimdall middleware handles JWT validation
  • Traefik IngressRoute for HTTP routing

CI/CD Pipeline

GitHub Actions workflows:

  • mega-linter.yml: Comprehensive linting (Go, YAML, Docker, etc.)
  • project-api-build.yml: Build and test on PRs
  • license-header-check.yml: Ensure proper licensing

PR Checks

  1. Generate API code
  2. Build binary
  3. Run unit tests
  4. Lint with MegaLinter

Common Pitfalls and Solutions

1. Forgetting to Generate Code

Problem: Changes to design files not reflected in implementation Solution: Always run make apigen after modifying design files

2. ETag Handling

Problem: Concurrent updates without proper ETag validation Solution: Always include If-Match header in PUT/DELETE requests (server responds with ETag header on GET request)

3. NATS Connection

Problem: Service fails to start due to NATS connection Solution: Ensure NATS is running and NATS_URL is correct

4. Slug Validation

Problem: Invalid slug format causes API errors Solution: Slugs must match ^[a-z][a-z0-9_\-]*[a-z0-9]$

5. Parent Project Validation

Problem: Creating projects with invalid parent_uid Solution: parent_uid must be empty string or valid UUID

Mock Data Loading

Use the provided script to load test data:

cd scripts/load_mock_data
go run main.go -bearer-token "your-token" -num-projects 10

Key Implementation Details

1. Project Data Split

Projects are split into two parts for access control:

  • Base: Core project info (stored in projects KV)
  • Settings: Sensitive settings (stored in project-settings KV)

2. Message Publishing

Every data modification publishes NATS messages:

  • Index messages for search service
  • Access control updates for authorization service

3. NATS Event Wire Types (pkg/events/)

NATS message payload types that other services consume belong in pkg/events/, not internal/. This lets downstream services (e.g., lfx-v2-invite-service) import the canonical struct definitions directly.

  • Domain types in internal/domain/models/ may differ from wire types and can evolve independently.
  • Explicit converter functions in internal/service/converters.go map from domain → event type before publishing.
  • Example: DomainSettingsToEvent(*models.ProjectSettings) events.ProjectSettings

Rule: if a struct appears in a NATS message payload, it belongs in pkg/events/, not internal/.

4. Request Context

Important context values:

  • request-id: Unique request identifier
  • authorization: JWT token from header
  • etag: ETag value for optimistic concurrency (sent as If-Match header in requests)

5. Error Handling

Domain errors are mapped to HTTP status codes:

  • ErrNotFound → 404
  • ErrConflict → 409
  • ErrInvalidParentUID → 400
  • ErrInternal → 500

Debugging Tips

  1. Enable Debug Logging: Run with -d flag or set LOG_LEVEL=debug
  2. Check NATS Messages: Use nats sub "lfx.>" to monitor all messages
  3. Verify KV Data: Use nats kv get projects <uid> to check stored data
  4. HTTP Traces: Middleware logs all requests with timing
  5. Generated Code: Check api/project/v1/gen/ directory for Goa-generated interfaces

Documentation Structure

The project has a clear documentation hierarchy:

  • README.md: Project overview, quick start, API endpoints, deployment setup
  • DEVELOPMENT.md: Comprehensive developer guide with build/test/deploy workflows
  • CLAUDE.md: AI assistant instructions and technical details (this file)

Key documentation patterns:

  • README focuses on getting the service running quickly
  • DEVELOPMENT.md covers the full development workflow
  • Avoid duplicating content between files - use cross-references instead

Contributing Guidelines

  1. Design First: Update Goa design files before implementation
  2. Test Coverage: Write comprehensive unit tests
  3. Mock External Deps: Use mocks for repository and message builder
  4. Follow Clean Architecture: Respect layer boundaries
  5. Update Docs: Keep documentation current and avoid duplication
  6. Lint Clean: Ensure make check passes

Resources