just generate
# or
go generate ./...
FUN-6 API-First Design
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
-
Design the API - Write the protobuf or OpenAPI specification
-
Review the specification - API changes go through PR review
-
Generate code - Use
go generate ./…orjust generateto produce server stubs and client libraries -
Implement - Write the business logic against the generated interfaces
-
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 |
|
Authentication, user management |
organization-api |
gRPC |
|
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 |
|---|---|---|
|
Retrieve a collection of resources |
|
|
Retrieve a single resource by ID |
|
|
Create a new resource |
|
|
Modify an existing resource |
|
|
Remove a resource (soft delete) |
|
|
Add a relationship or child resource |
|
|
Remove a relationship or child resource |
|
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.Emptyfor 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>Summaryfor list items and<Object>Detailsfor 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
UNSPECIFIEDzero value -
Example:
ClusterStatus→CLUSTER_STATUS_UNSPECIFIED,CLUSTER_STATUS_RUNNING
Fields
-
IDs: Use
<object>_idfor 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
optionalkeyword for nullable values -
Empty requests/responses: Use
google.protobuf.Emptyfor 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:
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 NULLindicates an active resource -
deleted IS NOT NULLindicates 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 uses PostgreSQL Row-Level Security (RLS) to enforce organization isolation at the database level. Each request sets the current organization context:
SET app.current_organization_id = '<uuid>';
RLS policies automatically filter queries to only return rows belonging to the current organization:
CREATE POLICY organization_isolation ON tenant.clusters
FOR ALL TO fun_fundament_api
USING (organization_id = current_setting('app.current_organization_id')::uuid);
This approach ensures:
-
Defense in depth: Tenant isolation enforced at database level, not just application code
-
No accidental leaks: Impossible to query another organization’s data without explicit context change
-
Simplified queries: Application code doesn’t need to add organization filters
Child resources (e.g., node_pools, installs) use policies that join to their parent to inherit organization isolation:
CREATE POLICY node_pools_organization_policy ON tenant.node_pools
USING (EXISTS (
SELECT 1 FROM clusters
WHERE clusters.id = node_pools.cluster_id
AND clusters.organization_id = current_setting('app.current_organization_id')::uuid
));
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
FieldMaskpattern for most use cases -
Explicit intent:
optionalmakes it clear which fields can be omitted -
Null safety: Unset fields are distinguishable from empty values in generated Go code (
*stringvsstring)
7. Related
-
FUN-4: Plugins expose resources through the API