LFX V2 Project Service

September 24, 2026 · View on GitHub

This repository contains the source code for the LFX v2 platform project service.

Overview

The LFX v2 Project Service is a RESTful API service that manages projects within the Linux Foundation's LFX platform. It provides endpoints for creating, reading, updating, and deleting projects with built-in authorization and audit capabilities.

API Endpoints

MethodPathDescriptionNotes
GET/readyzReadiness check
GET/livezLiveness check
GET/projectsList all projectsWill be removed in favour of the query service
POST/projectsCreate a project
GET/projects/:idGet project base info
PUT/projects/:idUpdate project base infoSee OpenAPI spec for updatable fields
DELETE/projects/:idDelete a project
GET/projects/:id/settingsGet project settings
PUT/projects/:id/settingsUpdate project settings
POST/projects/:id/linksCreate a link
GET/projects/:id/links/:link_uidGet a linkReturns ETag header
DELETE/projects/:id/links/:link_uidDelete a linkRequires If-Match: <etag>
POST/projects/:id/foldersCreate a folderName must be unique per project
GET/projects/:id/folders/:folder_uidGet a folderReturns ETag header
DELETE/projects/:id/folders/:folder_uidDelete a folderRequires If-Match: <etag>; blocked if folder has contents
POST/projects/:id/documentsUpload a documentmultipart/form-data: name, description, folder_uid, file; max 10 MB; PDF, DOC/DOCX, XLS/XLSX, PPT/PPTX, TXT, CSV, PNG, JPEG, GIF, ZIP
GET/projects/:id/documents/:document_uidGet document metadataReturns ETag header
DELETE/projects/:id/documents/:document_uidDelete a documentRequires If-Match: <etag>
GET/projects/:id/documents/:document_uid/downloadDownload document binaryReturns Content-Disposition: attachment with original filename
GET/projects/slug-to-uid/:slugResolve a project slug to its UIDRequires authentication; returns { "uid": "..." }. Internal-only HTTP transport adapter: exists because Heimdall's project_slug_resolver_contextualizer (defined in lfx-v2-helm, used by services like lfx-v2-campaign-service to authorize project-nested routes on UID-keyed OpenFGA tuples when the URL only carries a slug) can call HTTP but not this service's NATS RPC (lfx.projects-api.slug_to_uid) that already backs the same lookup. Not intended for direct end-user or browser use; its Heimdall rule intentionally omits anonymous_authenticator so anonymous callers cannot reach it.

NATS Message Handlers

Request/reply RPC subjects — callers block waiting for a response:

SubjectDescription
lfx.projects-api.get_nameGet a project name from a given project UID
lfx.projects-api.get_slugGet a project slug from a given project UID
lfx.projects-api.get_logoGet a project logo URL from a given project UID
lfx.projects-api.get_parent_uidGet a project's parent UID from a given project UID
lfx.projects-api.get_writersGet a project's configured writers from a given project UID
lfx.projects-api.get_settingsGet a project's writers, auditors and announcement date from a given project UID
lfx.projects-api.list_projectsList projects at given stages, by given UIDs, or the union of both
lfx.projects-api.slug_to_uidGet a project UID from a given project slug

get_settings returns the grant roster and the announcement date only, not the whole settings record; get_writers remains the cheaper call when the writers alone will do.

list_projects takes a JSON body with stages and/or uids, at least one of which must be set, and returns their union. The stage filter scans the project store, so a request carrying only uids is the cheaper shape, and may name at most 500 distinct projects — a larger list is refused rather than truncated, so a caller never mistakes a capped reply for a complete one. Repeats are ignored rather than counted against the limit. The reply is not filtered on project visibility, so confidential projects are included: treat this subject as trusted service-to-service only, and do not relay its reply to an unauthorized user.

Error responses

All request/reply subjects above share the same error-reply contract. When a handler encounters an error it responds with a JSON object instead of the normal success payload:

{"error": "<code>", "message": "<human-readable detail>"}
CodeMeaning
not_foundThe requested resource does not exist — treat as a confirmed absence.
internalAny other service error (infrastructure failure, bad-request condition, etc.) — treat as unrecoverable; the only distinction this contract makes is between a confirmed absent resource and everything else.

The message field is present on not_found replies and omitted or set to a generic string on internal replies; callers must not parse it programmatically.

For most subjects, a nil or empty reply body is never produced intentionally. If a caller receives one it indicates a dispatch or transport failure and should be treated as an unrecoverable error.

Subject-specific empty-reply exceptions (treat as a valid success, not a transport failure):

SubjectEmpty-body meaning
lfx.projects-api.get_parent_uidProject is a root project (parent_uid == "").
lfx.projects-api.get_logoProject has no logo set (logo_url == "").

The pkg/events package exports ParseRPCError and the ErrRPCNotFound / ErrRPCInternal sentinels so consuming services can handle error envelopes without restating the JSON logic.

NATS Inbound Event Subscriptions

Fire-and-forget event subscribers — no reply is sent to the publisher:

SubjectPublisherEffect
lfx.projects-api.project_settings.updatedSelfSends role-notification emails or invite requests when project membership changes
lfx.invite-service.invite_acceptedinvite-servicePromotes matching email-only members to full LFID membership across all their projects
lfx.projects-api.project_document.createdSelfEmails project writers and auditors about the newly uploaded document
lfx.projects-api.project_link.createdSelfEmails project writers and auditors about the newly added link

NATS Events Published

This service publishes the following NATS events:

Project Data Events

  • lfx.projects-api.project_settings.updated: Published when project settings are updated and when a project is created with initial notification recipients. old_settings and new_settings always carry complete ProjectSettings snapshots for downstream consumers; notification_roles only scopes which roles this service should notify, and an omitted or empty notification_roles means "all roles". Message format:

    {
      "project_uid": "string",
      "old_settings": { /* ProjectSettings object */ },
      "new_settings": { /* ProjectSettings object */ },
      "notification_roles": ["mentorship_program_admin"]
    }
    
  • lfx.projects-api.project_document.created: Published when a file document is uploaded. Triggers email notifications to project writers and auditors.

  • lfx.projects-api.project_link.created: Published when a link is added to a project. Triggers email notifications to project writers and auditors.

Indexer Contract

This service indexes project data into the indexer service, making it searchable via the query service.

  • lfx.index.project: Published when a project is created, updated, or deleted. Contains the project base data and tags for indexing.
  • lfx.index.project_settings: Published when project settings are created or updated, and when the project is deleted. Contains the project settings data and tags for indexing.
  • lfx.index.project_link: Published when a project link is created or deleted.
  • lfx.index.project_folder: Published when a project folder is created or deleted.
  • lfx.index.project_document: Published when a project document is uploaded or deleted.

Create and update indexer messages include an IndexingConfig that provides the metadata controlling how the document is stored, searched, and access-checked in the index. Project and project-settings delete messages send the bare UID; link, folder, and document delete messages include IndexingConfig with the parent project access metadata. For the full field reference and message format details, see the indexer service client guide.

For the data schemas, tags, access control values, parent references, and fulltext fields for all resource types — see docs/indexer-contract.md.

FGA Sync Contract

This service uses the generic FGA sync handlers for managing fine-grained access control. All access control messages use the GenericFGAMessage envelope format. For the full authoritative reference, see docs/fga-contract.md.

  • lfx.fga-sync.update_access: Published when project access permissions are updated. This is a full sync operation - any relations not included will be removed. Message format:

    {
      "object_type": "project",
      "operation": "update_access",
      "data": {
        "uid": "project-uid",
        "public": true,
        "relations": {
          "writer": ["username1", "username2"],
          "auditor": ["username3"],
          "meeting_coordinator": ["username4"],
          "executive_director": ["username5"]
        },
        "references": {
          "parent": ["project:parent-uid"]
        }
      }
    }
    
  • lfx.fga-sync.delete_access: Published when a project is deleted. Removes all access control tuples for the project. Message format:

    {
      "object_type": "project",
      "operation": "delete_access",
      "data": {
        "uid": "project-uid"
      }
    }
    

Quick Start

Pre-requisites

  • Kubernetes
  • Helm

Setup

  1. Install the lfx-platform helm chart from lfx-v2-helm repo. This is a general helm chart that is used for all LFX platform services. It contains all of the dependencies packaged in kubernetes that are needed by the platform: NATS, Heimdall, Authelia, Traefik, etc..

    Either read the official instructions from the repo containing the chart, or run the commands below:

    # Create namespace (recommended). You should use this for all LFX services. You may already have the namespace created if you have worked on another LFX service. In that case, you can proceed to the next command.
    kubectl create namespace lfx
    
    # Install the chart via the OCI registry.
    # Note: change the version to use the latest (or desired) chart version according to the releases for the lfx-platform chart: https://github.com/linuxfoundation/lfx-v2-helm/pkgs/container/lfx-v2-helm%2Fchart%2Flfx-platform
    helm install -n lfx lfx-platform \
    oci://ghcr.io/linuxfoundation/lfx-v2-helm/chart/lfx-platform \
    --version 0.1.12
    
  2. Install the lfx-v2-project-service helm chart from this current repository. You have two options: either install from the OCI registry or from the source. If you don't plan to develop the service, you can just use the packaged version from the github packages.

    # From OCI registry
    # Note: check the latest (or desired) version from https://github.com/linuxfoundation/lfx-v2-project-service/pkgs/container/lfx-v2-project-service%2Fchart%2Flfx-v2-project-service
    helm install -n lfx lfx-v2-project-service \
    oci://ghcr.io/linuxfoundation/lfx-v2-project-service/chart/lfx-v2-project-service \
    --version 0.4.0
    
    # From source (current local directory)
    helm install -n lfx lfx-v2-project-service ./charts/lfx-v2-project-service
    
  3. After installing the required helm charts, you should have the project REST API running on your machine in kubernetes, and can therefore start making some requests to the API.

Making requests to the API

  1. Get an ID token from the Authelia IdP server.

    In order to make a request to the service via Traefik, you need to be making an authenticated request as a valid Authelia user. If you have the lfx-platform chart installed from the previous steps, then you can use the kubectl CLI tool to get the list of users that you can use for authentication. They are stored in kubernetes as a secret resource.

    kubectl get secret authelia-users -n lfx -o json
    

    The list of users in Authelia are set by the lfx-platform chart to help for testing basic scenarios. You can find the users and how they are set up in Authelia from lfx-v2-helm repo chart Currently, the list is as follows:

    committee_member_1
    committee_member_2
    project_admin_1
    project_admin_2
    project_super_admin
    

    Currently, you should use the existing token helper script to generate the ID token. The script is only accessible if you are LF staff. The team has a TODO in order to include the helper script in a public repo or come up with a better solution for generating ID tokens for local testing.

    If you have access to the token helper script, run the following command to get the ID token. Note that you will be prompted in your web browser to log in as one of the valid Authelia users. Use the kubernetes secret authelia-users as previously mentioned to determine the password for each user. Use the username and password for the user you want to authenticate with.

    id_token=$(./token_helper.py); echo $id_token
    
  2. Use the ID token in the Authorization Header to make a request to the project service

    You can find documentation about the list of API endpoints supported by the service by looking at the OpenAPI specification file

    For now, try to make a request to list the projects:

    curl -H "Authorization: Bearer $id_token" http://lfx-api.k8s.orb.local/projects
    

    You should get a response as follows. Running the app container via the lfx-v2-project-service Helm chart should run an init container that creates a root project. The UID will be a random UUID, but the slug, description, and other fields should be the same.

    {
    "projects": [
       {
          "uid": "81570bff-3267-4942-80f3-d469437a46d6",
          "slug": "ROOT",
          "description": "A root project for teams permissions assignment, ordinarily hidden from users.",
          "name": "ROOT",
          "public": false,
          "autojoin_enabled": false,
          "created_at": "2025-07-31T00:41:54Z",
          "updated_at": "2025-07-31T00:41:54Z",
          "mission_statement": "A root project for teams permissions assignment, ordinarily hidden from users."
       }
    ]
    }
    

    If you get a 403 Forbidden error, then you need to check that the ID token you are passing to the project service is valid and not expired. Once you have an ID token, you can check its expiration and other user metadata on the token using this auth server API call:

    curl -s https://auth.k8s.orb.local/api/oidc/userinfo \
       -H "Authorization: Bearer $id_token" |
       jq -c .
    

    Next, try to create a project:

    curl -X POST http://lfx-api.k8s.orb.local/projects \
       -H "Authorization: Bearer $id_token" \
       -H "Content-Type: application/json" \
       -d '{
          "name": "My Test Project",
          "slug": "my-test-project",
          "description": "A test project created via API",
          "parent_uid": "<ROOT_PROJECT_UID_GOES_HERE>",
          "public": false,
          "autojoin_enabled": false
       }'
    

    You should get a response like:

    {
    "uid": "7bdc6e40-8cc8-4536-b537-e6cd31ce058d",
    "slug": "my-test-project",
    "description": "A test project created via API",
    "name": "My Test Project",
    "public": false,
    "parent_uid": "81570bff-3267-4942-80f3-d469437a46d6",
    "autojoin_enabled": false,
    "created_at": "2025-08-12T19:43:24Z",
    "updated_at": "2025-08-12T19:43:24Z"
    }
    

    Then try to get the newly created project:

    curl -H "Authorization: Bearer $id_token" http://lfx-api.k8s.orb.local/projects/<NEW_PROJECT_UID_GOES_HERE>
    

    You should get a response just like the POST project endpoint:

    {
    "uid": "7bdc6e40-8cc8-4536-b537-e6cd31ce058d",
    "slug": "my-test-project",
    "description": "A test project created via API",
    "name": "My Test Project",
    "public": false,
    "parent_uid": "81570bff-3267-4942-80f3-d469437a46d6",
    "autojoin_enabled": false,
    "created_at": "2025-08-12T19:43:24Z",
    "updated_at": "2025-08-12T19:43:24Z"
    }
    

File Structure

├── .github/                        # Github files
│   └── workflows/                  # Github Action workflow files
├── api/                            # API contracts and specifications
│   └── project/                    # Project service API
│       └── v1/                     # API version 1
│           ├── design/             # Goa API design specifications
│           └── gen/                # Generated code from Goa design
├── charts/                         # Helm charts for running the service in kubernetes
├── cmd/                            # Services (main packages)
│   ├── project-api/                # Project service API entry point
│   └── project-cli/                # Operational CLI for sync/migration jobs
├── internal/                       # Internal service packages
│   ├── domain/                     # Domain logic layer (business logic)
│   │   └── models/                 # Domain models and entities
│   ├── service/                    # Service logic layer (service implementations)
│   └── infrastructure/             # Infrastructure layer
│       ├── auth/                   # Authentication abstractions
│       ├── log/                    # Logging utilities
│       ├── middleware/             # HTTP middleware components
│       ├── nats/                   # NATS messaging and repository implementation
│       └── opensearch/             # OpenSearch client
└── pkg/                            # Shared packages
    └── constants/                  # Shared constants and configurations

Development

Before making any changes, read CLAUDE.md — it is the authoritative guide for AI agents and human contributors alike. Non-Claude AI tools should read AGENTS.md, which redirects to the same guide.

To contribute to this repository:

  1. Fork the repository and install the git hooks: make hooks (or make deps which also installs them).
  2. Commit your changes to a feature branch. Branch names should follow the pattern type/LFXV2-NNNN-short-topic.
  3. Write commit messages following Angular conventional commits:
    type(scope): summary [LFXV2-NNNN]
    
    Valid types: feat, fix, docs, test, refactor, chore, build, ci, perf, style, revert.
  4. Every commit must be signed with both a GPG signature and a DCO sign-off:
    git commit -s -S
    
    See CLAUDE.md for one-time GPG setup instructions.
  5. Ensure the chart version in charts/lfx-v2-project-service/Chart.yaml has been updated following semantic version conventions if you are making changes to the chart.
  6. Submit your pull request. PR titles follow the same type(scope): summary format.

For more details about development on this repository, read the DEVELOPMENT.md.

Releases

Creating a Release

To create a new release of the project service:

  1. Update the chart version in charts/lfx-v2-project-service/Chart.yaml prior to any project releases, or if any change is made to the chart manifests or configuration:

    version: 0.2.0  # Increment this version
    appVersion: "latest"  # Keep this as "latest"
    
  2. After the pull request is merged, create a GitHub release and choose the option for GitHub to also tag the repository. The tag must follow the format v{version} (e.g., v0.2.0). The tag version used will be the same as the chart version and app version for the helm chart.

  3. The GitHub Actions workflow will automatically:

    • Build and publish the container images (project-api, project-cli, and root-project-setup)
    • Package and publish the Helm chart to GitHub Pages
    • Publish the chart to GitHub Container Registry (GHCR)
    • Sign the chart with Cosign
    • Generate SLSA provenance
    • Dispatch create-version-bump-pr.yml on lfx-v2-argocd so staging and prod image tags and chart pins are opened as a pull request

Important Notes

  • The appVersion in Chart.yaml should always remain "latest" in the committed code.
  • During the release process, the ko-build-tag.yaml workflow automatically overrides the appVersion and version with the actual tag version (e.g., v0.2.0 becomes 0.2.0).
  • The container image tags are automatically managed by the consolidated CI/CD pipeline using the git tag.
  • Container images (project-api, project-cli, and root-project-setup) and the Helm chart are published together in a single workflow.

License

Copyright The Linux Foundation and each contributor to LFX.

This project’s source code is licensed under the MIT License. A copy of the license is available in LICENSE.

This project's documentation is licensed under the Creative Commons Attribution 4.0 International License (CC-BY-4.0). A copy of the license is available in LICENSE-docs.

Security

See SECURITY.md for vulnerability reporting and the security policy for this repository.