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
- Database Independence: Repository interfaces allow switching storage backends
- Testability: Each layer can be tested in isolation using mocks
- Event-Driven: All data changes trigger NATS messages for downstream services
- 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:
- Design First: API is defined in
api/project/v1/design/files - Generated Code: Running
make apigengenerates toapi/project/v1/gen/:- HTTP server/client code
- Service interfaces
- OpenAPI specifications
- Type definitions
- Implementation: You implement the generated interfaces in
cmd/project-api/service*.gofiles
Adding New Endpoints
- Update
api/project/v1/design/project.gowith new method - Run
make apigen(from repository root) to regenerate code - Implement the new method in
cmd/project-api/service_endpoint_project.go - Add tests in
cmd/project-api/service_endpoint_project_test.go - Update Heimdall ruleset in
charts/*/templates/ruleset.yaml
NATS Messaging Patterns
The service uses NATS for:
- Storage: Key-Value stores for project data
- Events: Publishing events on data changes
- RPC: Handling requests from other services
Key-Value Stores
projects: Base project informationproject-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.,
SendIndexProject→TestMessageBuilder_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
| Variable | Description | Default | Required |
|---|---|---|---|
PORT | HTTP listen port | 8080 | No |
NATS_URL | NATS server URL | nats://localhost:4222 | No |
LOG_LEVEL | Log level | info | No |
JWKS_URL | JWT verification endpoint | - | No |
AUDIENCE | JWT audience | lfx-v2-project-service | No |
JWT_AUTH_DISABLED_MOCK_LOCAL_PRINCIPAL | Mock auth for local dev | - | No |
LFX_SELF_SERVE_BASE_URL | Base URL for project links in notification emails | derived from LFX_ENVIRONMENT | No |
EMAILS_ENABLED | Gate for outbound role-notification emails to LFID users (true to enable) | false | No |
INVITES_ENABLED | Gate for outbound invite requests to non-LFID users (true to enable) | false | No |
Authorization (OpenFGA)
When deployed, the service uses OpenFGA for authorization:
- GET /projects - No check (public list)
- POST /projects - Requires
writeron parent (if specified) - GET /projects/:id - Requires
vieweron project - GET /projects/:id/settings - Requires
auditoron project - PUT /projects/:id - Requires
writeron project - PUT /projects/:id/settings - Requires
writeron project - DELETE /projects/:id - Requires
owneron 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-localwith 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 valuesmake helm-restart: Restart deployment podmake 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
- Generate API code
- Build binary
- Run unit tests
- 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
projectsKV) - Settings: Sensitive settings (stored in
project-settingsKV)
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.gomap 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 identifierauthorization: JWT token from headeretag: ETag value for optimistic concurrency (sent as If-Match header in requests)
5. Error Handling
Domain errors are mapped to HTTP status codes:
ErrNotFound→ 404ErrConflict→ 409ErrInvalidParentUID→ 400ErrInternal→ 500
Debugging Tips
- Enable Debug Logging: Run with
-dflag or setLOG_LEVEL=debug - Check NATS Messages: Use
nats sub "lfx.>"to monitor all messages - Verify KV Data: Use
nats kv get projects <uid>to check stored data - HTTP Traces: Middleware logs all requests with timing
- 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
- Design First: Update Goa design files before implementation
- Test Coverage: Write comprehensive unit tests
- Mock External Deps: Use mocks for repository and message builder
- Follow Clean Architecture: Respect layer boundaries
- Update Docs: Keep documentation current and avoid duplication
- Lint Clean: Ensure
make checkpasses