Skip to content

FUN-6 API-First Design

Pre-Discussion

1. Introduction

This FUN documents the decision to adopt an API-first design approach for Fundament. In API-first development, the API specification is written before implementing any code. The specification becomes the contract that drives both backend implementation and client generation.

2. Why API-First

2.1. Multiple Clients, Single Source of Truth

Fundament needs to support multiple clients:

  • Console UI (web interface)

  • CLI tooling

  • Infrastructure-as-code integrations

  • Third-party integrations

Without a well-defined API contract, each client would interpret the backend differently, leading to inconsistencies and integration bugs. By defining the API first, all clients work from the same specification.

2.2. Developer Experience

API-first enables:

  • Generated client libraries in multiple languages

  • Auto-generated documentation

  • Contract-based testing

  • Parallel development (frontend and backend can work simultaneously)

2.3. Change Management

When the API specification is the source of truth:

  • Breaking changes are explicit and visible in the spec diff

  • Versioning is enforced at the API level

  • Deprecation can be communicated through the spec

3. Technology Choices

3.1. Protocol Buffers and gRPC

For service-to-service communication and the primary API definition, we use Protocol Buffers (protobuf) with gRPC:

  • Strong typing with code generation

  • Efficient binary serialization

  • Built-in versioning through protobuf evolution rules

  • gRPC-Gateway for REST/JSON translation

3.2. OpenAPI for HTTP APIs

Where HTTP/REST is more appropriate (authentication, webhooks), OpenAPI specifications are used:

  • Wide tooling ecosystem

  • Human-readable documentation

  • Client generation for languages without good gRPC support

4. Workflow

  1. Design the API - Write the protobuf or OpenAPI specification

  2. Review the specification - API changes go through PR review

  3. Generate code - Use go generate ./…​ or just generate to produce server stubs and client libraries

  4. Implement - Write the business logic against the generated interfaces

  5. Test - Contract tests verify the implementation matches the spec

5. Implications

5.1. No Hand-Written API Code

Generated code must not be manually edited. All API types, server interfaces, and client code come from the specification. This ensures the spec and implementation cannot drift.

5.2. Specification Lives in Version Control

API specifications are tracked in git alongside the code. This provides:

  • History of API evolution

  • Code review for API changes

  • Single source of truth

5.3. Breaking Changes Require Consideration

Because the API is a contract with multiple clients, breaking changes must be:

  • Discussed before implementation

  • Versioned appropriately (new version or deprecation period)

  • Communicated to API consumers

6. Current API Design

This section documents the API specifications in Fundament.

6.1. API Overview

Fundament exposes two primary APIs:

API Type Location Purpose

authn-api

gRPC + OpenAPI

authn-api/pkg/proto/ + authn-api/openapi.yaml

Authentication, user management

organization-api

gRPC

organization-api/pkg/proto/v1/

Organizations, clusters, node pools, plugins

All gRPC APIs use Connect RPC for HTTP/1.1 compatibility.

6.2. Design Conventions

6.2.1. Naming Conventions

RPC Methods

RPC methods follow the <Action><Object> pattern (PascalCase):

Action Description Example

List

Retrieve a collection of resources

ListClusters, ListNodePools

Get

Retrieve a single resource by ID

GetCluster, GetNodePool

Create

Create a new resource

CreateCluster, CreateNodePool

Update

Modify an existing resource

UpdateCluster, UpdateNodePool

Delete

Remove a resource (soft delete)

DeleteCluster, DeleteNodePool

Add

Add a relationship or child resource

AddInstall

Remove

Remove a relationship or child resource

RemoveInstall

Messages

Messages follow consistent naming patterns:

  • Request messages: <Action><Object>Request (e.g., GetClusterRequest, CreateNodePoolRequest)

  • Response messages: <Action><Object>Response (e.g., GetClusterResponse, ListClustersResponse)

  • Void responses: Use google.protobuf.Empty for operations that return no data (e.g., Update, Delete, Remove)

  • Resource messages: Singular noun (e.g., Cluster, NodePool, Organization, Install)

  • Summary vs Detail: Use <Object>Summary for list items and <Object>Details for full resource (e.g., ClusterSummary, ClusterDetails)

Enums
  • Prefix: Enum values are prefixed with the enum name in SCREAMING_SNAKE_CASE

  • Zero value: Always include an UNSPECIFIED zero value

  • Example: ClusterStatusCLUSTER_STATUS_UNSPECIFIED, CLUSTER_STATUS_RUNNING

Fields
  • IDs: Use <object>_id for foreign key references (e.g., cluster_id, node_pool_id, plugin_id)

  • Timestamps: Use past tense for timestamp fields (e.g., created, deleted, revoked)

  • snake_case: All field names use snake_case (e.g., kubernetes_version, machine_type, autoscale_min)

Identifiers (UUIDv7)

All resource identifiers use UUIDv7. This format embeds a Unix timestamp in the first 48 bits, providing:

  • Time-ordered: IDs sort chronologically, improving database index locality and query performance

  • Globally unique: No coordination required between services

  • Timestamp extraction: Creation time can be derived from the ID without additional fields

  • K-sortable: Lexicographic sorting matches chronological order

The database generates UUIDv7 values using DEFAULT uuidv7() on primary key columns.

6.2.2. Protocol Buffers

  • Package naming: <domain>.v1 (e.g., organization.v1, authn.v1)

  • Field numbering: Start at 10, increment by 10 for room to insert fields

  • Optional fields: Use optional keyword for nullable values

  • Empty requests/responses: Use google.protobuf.Empty for void operations

6.2.3. OpenAPI

  • Version: OpenAPI 3.0.3

  • Responses: Standard error responses (BadRequest, Unauthorized, InternalServerError)

  • Schemas: Reusable components in #/components/schemas

6.2.4. Code Generation

Generate code using:

just generate
# or
go generate ./...

Generated code locations:

  • Proto → pkg/proto/gen/

  • OpenAPI → pkg/*/server.gen.go (via oapi-codegen)

6.3. Data Patterns

6.3.1. Soft Deletes

Resources are never physically deleted from the database. Instead, a deleted timestamp column marks when a resource was removed:

  • deleted IS NULL indicates an active resource

  • deleted IS NOT NULL indicates a deleted resource

This pattern provides:

  • Audit trail: History of all resources is preserved

  • Recovery: Deleted resources can be restored

  • Referential integrity: Foreign key relationships remain valid

Unique constraints use NULLS NOT DISTINCT to allow multiple deleted resources with the same name while ensuring active resources have unique names:

CONSTRAINT clusters_uq_name UNIQUE NULLS NOT DISTINCT (organization_id, name, deleted)

6.3.2. Multi-tenancy

Fundament enforces organization isolation at the application level using a combination of HTTP headers, JWT validation, and OpenFGA authorization checks.

Every organization-scoped API request must include the Fun-Organization HTTP header carrying the active organization’s UUID. The auth interceptor processes this header on each request:

  1. It extracts and validates the JWT from the Authorization header (or cookie)

  2. It parses the Fun-Organization header and confirms the user belongs to that organization (checking against the organization IDs in the JWT claims)

  3. It injects the validated organization ID and user ID into the request context

Queries then scope results to the active organization through application-level filtering. Every database query for organization-scoped resources includes the organization ID from the request context.

On top of this scoping, OpenFGA provides fine-grained permission checks (can_view, can_edit, can_delete, etc.) based on the user’s relationship to the resource being accessed. See FUN-7 for the full permission model.

User-scoped endpoints (ListOrganizations, ListInvitations, AcceptInvitation, DeclineInvitation) do not require the Fun-Organization header since they operate across all of the user’s organizations.

This approach provides:

  • Clear request-scoped context: The organization boundary is explicit in every request via the HTTP header, making it easy to trace and debug

  • Auditable authorization: OpenFGA provides relationship-based authorization with a clear model that can be reasoned about and tested

  • Straightforward database access: Queries use standard WHERE clauses with the organization ID from context, with no database-level policy magic

6.3.3. Partial Updates

Update operations use the optional keyword in protobuf to support partial updates. Only fields that are explicitly set in the request are modified:

message UpdateClusterRequest {
  string cluster_id = 10;
  optional string kubernetes_version = 20;
}

This pattern:

  • Avoids field masks: Simpler than Google’s FieldMask pattern for most use cases

  • Explicit intent: optional makes it clear which fields can be omitted

  • Null safety: Unset fields are distinguishable from empty values in generated Go code (*string vs string)