Development Guide
May 6, 2026 ยท View on GitHub
This guide provides comprehensive instructions for developers working on the LFX V2 Project Service, including building, testing, and deploying the service locally.
Prerequisites
Before you begin development, ensure you have the following tools installed:
-
Go 1.23+ - Download and install Go
-
Goa v3 - Code generation framework
go install goa.design/goa/v3/cmd/goa@latest -
golangci-lint - Linting tool
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest -
Docker - For building container images
-
Kubernetes - For local deployment (optional)
-
Helm - For deploying to Kubernetes (optional)
Development Setup Options
You have two main options for setting up your development environment:
Option A: Full Platform Setup (Recommended for Integration Testing)
If you need to test with the full LFX platform stack (including Heimdall, OpenFGA, Authelia, Traefik), first install the lfx-platform chart:
# Create namespace
kubectl create namespace lfx
# Install lfx-platform chart (includes all dependencies)
helm install -n lfx lfx-platform \
oci://ghcr.io/linuxfoundation/lfx-v2-helm/chart/lfx-platform \
--version 0.1.12
This provides:
- NATS with JetStream (messaging and storage)
- Heimdall (authorization middleware)
- OpenFGA (fine-grained access control)
- Authelia (authentication)
- Traefik (HTTP routing)
Then follow the Helm Local Development section below.
Option B: Minimal Setup (Recommended for Quick Development)
For rapid development and testing without the full platform:
# Just run NATS locally
docker run -d -p 4222:4222 nats:latest -js
Insert Root Project
All projects that are created require a parent_uid to be set. This means that if there are no existing projects, it is impossible to create a new project. To resolve this, you can insert a root project when developing locally using the root_project_setup tooling.
This will create a root project with a randomly generated UID and a slug of ROOT which is expected to be used as the root of the project hierarchy.
The helm chart runs this automatically as an init container.
This is sufficient for:
- Local development
- Unit testing
- API endpoint testing (with mock auth)
Note: this means that there is no authentication/authorization with heimdall. So only do this if you are developing the service without a need for testing the full flow of authentication.
Then follow the Local Development Without Kubernetes section below.
Development Workflow
1. Code Generation
This service uses Goa v3 for API code generation. The API design is defined in api/project/v1/design/ and code is generated to api/project/v1/gen/.
Important: Always regenerate code after modifying design files!
# Generate API code from design files
make apigen
# This runs: goa gen github.com/linuxfoundation/lfx-v2-project-service/api/project/v1/design -o api/project/v1
The generated code includes:
- HTTP server and client code
- Service interfaces
- OpenAPI specifications
- Type definitions
2. Building the Service
# Build the binary
make build
# This creates: bin/project-api
3. Running the Service
Environment Variables
Configure the service using the environment variables from .env.example and any modifications needed in a copied .env file:
cp .env.example .env
source .env
Option A: Direct Go Run
# Run with default settings
make run
# Run with debug logging
make debug
# Run with custom flags
go run ./cmd/project-api -d -p 8080
Option B: Using the Built Binary
# Build first
make build
# Run the binary
./bin/project-api -d
4. Linting and Formatting
# Format code
make fmt
# Run linter
make lint
# Check formatting and linting without modifying files
make check
5. Testing
# Run unit tests
make test
# Run tests with verbose output
make test-verbose
# Run tests with coverage report
make test-coverage
# Run specific test
go test -v ./cmd/project-api -run TestCreateProject
6. Clean Build Artifacts
# Remove generated files and binaries
make clean
Making Code Changes
Typical Development Cycle
-
Modify API Design (if changing the API)
# Edit files in api/project/v1/design/ vim api/project/v1/design/project.go -
Generate Code
make apigen -
Implement Changes
# Implement service endpoints vim cmd/project-api/service_endpoint_project.go # Add/modify business logic vim internal/service/project_service.go -
Test Your Changes
# Run tests make test # Check linting make check -
Run Locally
# Run the service to make API requests to the server to check behavior make debug -
Test the API
# Health check curl http://localhost:8080/livez # List projects curl http://localhost:8080/projects
Docker Development
Building the Docker Image
# Option 1: Using make command
make docker-build
# Option 2: Using docker directly
docker build -t linuxfoundation/lfx-v2-project-service:latest .
# The Dockerfile uses:
# - Multi-stage build for minimal image size
# - Chainguard Go image for building
# - Chainguard static image for runtime (distroless)
Running with Docker
# Run NATS first
docker run -d --name nats -p 4222:4222 nats:latest -js
# Run the service
docker run -d \
--name project-service \
-p 8080:8080 \
-e NATS_URL=nats://host.docker.internal:4222 \
-e JWT_AUTH_DISABLED_MOCK_LOCAL_PRINCIPAL=test-user \
lfx-v2-project-service:latest
# Check logs
docker logs project-service
# Test the service
curl http://localhost:8080/livez
Helm Local Development
Using Local Values File
For local development with Helm, you'll need to create a custom values file:
-
Create a local values file
# Copy the default values as a starting point cp charts/lfx-v2-project-service/values.yaml charts/lfx-v2-project-service/values.local.yaml -
Modify values.local.yaml for local development
# Edit charts/lfx-v2-project-service/values.local.yaml to use the local docker image repository image: repository: linuxfoundation/lfx-v2-project-service tag: latest pullPolicy: Never # Don't try to pull from registry # Add any other local overrides
Building and Deploying Locally
-
Build Local Docker Image
# Build with a local tag (matching the repository in values.local.yaml) docker build -t linuxfoundation/lfx-v2-project-service:latest . # Optional: If using minikube, load the image minikube image load linuxfoundation/lfx-v2-project-service:latest # Optional: If using kind, load the image kind load docker-image linuxfoundation/lfx-v2-project-service:latest -
Install/Upgrade Helm Chart with Local Values
make helm-install-local -
Verify Deployment
# Check pod status kubectl get pods -n lfx | grep project-service # Check logs kubectl logs -n lfx -l app=lfx-v2-project-service # Port forward for testing kubectl port-forward -n lfx svc/lfx-v2-project-service 8080:80 # Test the service curl http://localhost:8080/projects
Quick Development Iteration
For rapid development iteration:
# 1. Make your code changes
# 2. Build and load new image
make docker-build
# 3. Restart the pod to use new image
make helm-restart
# 4. Watch the logs
kubectl logs -f -n lfx -l app=lfx-v2-project-service
Local Development Without Kubernetes
For lightweight local development without Kubernetes:
NOTE: When running the service directly (outside of Kubernetes/Helm), you're bypassing:
- Traefik load balancer and routing
- Heimdall middleware for authentication and authorization
- OpenFGA fine-grained access control
This means:
- All API requests will bypass authentication checks
- The
JWT_AUTH_DISABLED_MOCK_LOCAL_PRINCIPALenvironment variable simulates a specific logged-in user
-
Start NATS with JetStream
docker run -d -p 4222:4222 nats:latest -js -
Create Required KV Stores
# Install NATS CLI go install github.com/nats-io/natscli/nats@latest # Create KV stores nats kv add projects --history=20 --storage=file nats kv add project-settings --history=20 --storage=file nats kv add project-links --history=20 --storage=file nats kv add project-folders --history=20 --storage=file nats kv add project-documents-metadata --history=20 --storage=file # Create Object Store for document binaries nats object add project-documents --storage=file -
Run the Service
# NATS server URL export NATS_URL=nats://localhost:4222 # Mock authentication - simulates a logged-in user (bypasses real auth) export JWT_AUTH_DISABLED_MOCK_LOCAL_PRINCIPAL=test-user # Enable debug logging export LOG_LEVEL=debug make run -
Test API Endpoints
# Create a project (no auth header needed with mock auth) curl -X POST http://localhost:8080/projects \ -H "Content-Type: application/json" \ -d '{ "name": "Test Project", "slug": "test-project", "description": "A test project" }' # List projects (no auth header needed with mock auth) curl http://localhost:8080/projects
Authorization with OpenFGA
When deployed via Kubernetes, the project service uses OpenFGA for fine-grained authorization control. The authorization is handled by Heimdall middleware before requests reach the service.
Configuration
OpenFGA authorization is controlled by the openfga.enabled value in the Helm chart:
# In values.yaml or via --set flag
openfga:
enabled: true # Enable OpenFGA authorization (default)
# enabled: false # Disable for local development only
Authorization Rules
When OpenFGA is enabled, the following authorization checks are enforced:
- GET /projects - No OpenFGA check (returns list of all projects)
- POST /projects - Requires
writerrelation on the parent project (if parent_uid is specified) - GET /projects/:id - Requires
viewerrelation on the specific project - GET /projects/:id/settings - Requires
auditorrelation on the specific project - PUT /projects/:id - Requires
writerrelation on the specific project - PUT /projects/:id/settings - Requires
writerrelation on the specific project - DELETE /projects/:id - Requires
ownerrelation on the specific project
Local Development
For local development without OpenFGA:
- Set
openfga.enabled: falsein your Helm values - All requests will be allowed through (after JWT authentication)
- Warning: Never disable OpenFGA in production environments
Architecture Overview
The service follows Clean Architecture principles. For the complete file structure, see the File Structure section in README.md.
Common Development Tasks
Adding a New Endpoint
-
Update API Design
// api/project/v1/design/project.go Method("NewEndpoint", func() { Description("New endpoint description") Payload(NewEndpointPayload) Result(NewEndpointResult) HTTP(func() { POST("/projects/{id}/action") Response(StatusOK) }) }) -
Generate Code
make apigen -
Implement Endpoint
// cmd/project-api/service_endpoint_project.go func (s *projectsrvc) NewEndpoint(ctx context.Context, p *projsvc.NewEndpointPayload) (*projsvc.NewEndpointResult, error) { // Implementation } -
Add Tests
// cmd/project-api/service_endpoint_project_test.go func TestNewEndpoint(t *testing.T) { // Test implementation } -
Update Heimdall Rules (if needed)
# charts/lfx-v2-project-service/templates/ruleset.yaml
Debugging Tips
-
Enable Debug Logging
export LOG_LEVEL=debug make run -
Monitor NATS Messages
# Subscribe to all LFX projects-api messages nats sub "lfx.projects-api.*" -
Check KV Store Data
# Get project data nats kv get projects <uid> # List all keys nats kv ls projects -
Use Request Tracing
# Send request with trace header curl -H "X-Request-ID: test-123" http://localhost:8080/projects
Troubleshooting
Common Issues
-
"goa: command not found"
go install goa.design/goa/v3/cmd/goa@latest export PATH=$PATH:$(go env GOPATH)/bin -
NATS Connection Failed
- Ensure NATS is running:
docker ps | grep nats - Check NATS_URL environment variable
- Verify network connectivity
- Ensure NATS is running:
-
Generated Code Out of Sync
- Always run
make apigenafter design changes - Check for uncommitted generated files
- Always run
-
Tests Failing
- Run
make clean && make apigento regenerate code - Check for missing mock expectations
- Run
Additional Resources
- Goa Framework Documentation
- NATS Documentation
- Project README - Quick start guide
- CLAUDE.md - AI assistant instructions and detailed technical documentation