Simple guides introducing Fabrica with hands-on examples.
Status: Implemented ✅ Version: 0.3.1+ Pattern: Kubernetes-inspired
Fabrica-generated microservices support Kubernetes-style status subresources, providing clear separation between desired state (spec) and observed state (status). This prevents conflicts between user updates and system controllers.
# Generate a project (status subresource is automatic)
fabrica init my-api --module github.com/example/my-api
fabrica add resource Device
fabrica generate
# Build and run
go mod tidy
go run ./cmd/server/
Generated Endpoints:
PUT /devices/{uid} → Update spec (user-facing)
PUT /devices/{uid}/status → Update status (system-facing)
PATCH /devices/{uid}/status → Patch status
Without status subresources, a single endpoint updates both spec and status:
// ❌ Old pattern - conflicts possible
PUT /devices/{uid}
{
"spec": {"location": "dc2"}, // User wants this
"status": {"phase": "Ready"} // Controller wants this
}
Issues:
Status subresources provide separate endpoints:
// ✅ New pattern - no conflicts
PUT /devices/{uid} // Users update spec only
{
"spec": {"location": "dc2"}
}
PUT /devices/{uid}/status // Controllers update status only
{
"status": {"phase": "Ready", "health": "Healthy"}
}
Benefits:
Update desired state:
# Update device location (spec)
curl -X PUT http://localhost:8080/devices/dev-123 \
-H "Content-Type: application/json" \
-d '{
"spec": {
"name": "sensor-01",
"location": "datacenter-2",
"model": "TempSensor-Pro"
}
}'
Patch specific fields:
# Patch just the location
curl -X PATCH http://localhost:8080/devices/dev-123 \
-H "Content-Type: application/merge-patch+json" \
-d '{
"spec": {
"location": "datacenter-3"
}
}'
Update observed state:
# Update device status
curl -X PUT http://localhost:8080/devices/dev-123/status \
-H "Content-Type: application/json" \
-d '{
"status": {
"phase": "Ready",
"health": "Healthy",
"lastSeen": "2025-10-24T16:00:00Z",
"conditions": [
{
"type": "Ready",
"status": "True",
"reason": "DeviceOnline",
"message": "Device is operational"
}
]
}
}'
Patch status fields:
# Patch just the health field
curl -X PATCH http://localhost:8080/devices/dev-123/status \
-H "Content-Type: application/merge-patch+json" \
-d '{
"status": {
"health": "Degraded"
}
}'
import (
"context"
"github.com/example/my-api/pkg/client"
"github.com/example/my-api/apis/example.fabrica.dev/v1/device"
)
func updateDeviceLocation(c *client.Client, uid string) error {
ctx := context.Background()
// Update spec only
spec := device.DeviceSpec{
Name: "sensor-01",
Location: "datacenter-2",
Model: "TempSensor-Pro",
}
req := client.UpdateDeviceRequest{
Name: "sensor-01",
DeviceSpec: spec,
}
device, err := c.UpdateDevice(ctx, uid, req)
if err != nil {
return err
}
fmt.Printf("Updated device spec: %s\n", device.Spec.Location)
return nil
}
func updateDeviceStatus(c *client.Client, uid string) error {
ctx := context.Background()
// Update status only
status := device.DeviceStatus{
Phase: "Ready",
Health: "Healthy",
Message: "Device is operational",
}
device, err := c.UpdateDeviceStatus(ctx, uid, status)
if err != nil {
return err
}
fmt.Printf("Updated device status: %s\n", device.Status.Phase)
return nil
}
func patchDeviceHealth(c *client.Client, uid string) error {
ctx := context.Background()
// Patch just the health field
patch := []byte(`{"health": "Degraded"}`)
device, err := c.PatchDeviceStatus(ctx, uid, patch)
if err != nil {
return err
}
fmt.Printf("Patched device health: %s\n", device.Status.Health)
return nil
}
Reconcilers automatically use status-only updates through the BaseReconciler.UpdateStatus() method.
package reconcilers
import (
"context"
"time"
"github.com/openchami/fabrica/pkg/reconcile"
"github.com/example/my-api/apis/example.fabrica.dev/v1/device"
)
type DeviceReconciler struct {
reconcile.BaseReconciler
}
func (r *DeviceReconciler) reconcileDevice(ctx context.Context, dev *device.Device) error {
// 1. Observe actual state (e.g., check if device is online)
isOnline := r.checkDeviceOnline(dev)
// 2. Update status based on observation
if isOnline {
dev.Status.Phase = "Ready"
dev.Status.Health = "Healthy"
dev.Status.LastSeen = time.Now().Format(time.RFC3339)
} else {
dev.Status.Phase = "Offline"
dev.Status.Health = "Unhealthy"
}
// 3. UpdateStatus automatically:
// - Loads fresh copy from storage
// - Preserves any concurrent spec changes
// - Only updates status fields
if err := r.UpdateStatus(ctx, dev); err != nil {
return err
}
return nil
}
func (r *DeviceReconciler) checkDeviceOnline(dev *device.Device) bool {
// Implement actual device health check
return true
}
The BaseReconciler.UpdateStatus() method ensures spec safety:
// From pkg/reconcile/reconciler.go
func (r *BaseReconciler) UpdateStatus(ctx context.Context, resource interface{}) error {
// 1. Extract resource UID
uid := resource.GetUID()
kind := resource.GetKind()
// 2. Load FRESH copy from storage (gets any concurrent spec updates)
current, err := r.Client.Get(ctx, kind, uid)
if err != nil {
return err
}
// 3. Copy status from reconciled resource to fresh resource
// (preserves fresh spec, applies reconciled status)
current.Status = resource.Status
current.Touch()
// 4. Save (spec is preserved, status is updated)
return r.Client.Update(ctx, current)
}
Define resources with separate Spec and Status using a flattened envelope:
package v1
import "github.com/openchami/fabrica/pkg/resource"
// Device represents a monitored device
type Device struct {
APIVersion string `json:"apiVersion"` // "infra.example.io/v1"
Kind string `json:"kind"` // "Device"
Metadata Metadata `json:"metadata"`
Spec DeviceSpec `json:"spec"`
Status DeviceStatus `json:"status,omitempty"`
}
// DeviceSpec defines desired state (user-managed)
type DeviceSpec struct {
Name string `json:"name" validate:"required"`
Location string `json:"location" validate:"required"`
Model string `json:"model,omitempty"`
Config map[string]string `json:"config,omitempty"`
}
// DeviceStatus defines observed state (system-managed)
type DeviceStatus struct {
// Phase represents the lifecycle phase
Phase string `json:"phase,omitempty"` // Pending, Ready, Offline, Error
// Health represents operational health
Health string `json:"health,omitempty"` // Healthy, Degraded, Unhealthy
// Message provides human-readable status
Message string `json:"message,omitempty"`
// LastSeen is when the device was last contacted
LastSeen string `json:"lastSeen,omitempty"`
// Conditions track specific state transitions
Conditions []resource.Condition `json:"conditions,omitempty"`
}
Status subresources support fine-grained authorization:
// In your custom authorization middleware
type DeviceAuthMiddleware struct {
// Your authorization implementation
}
// Users can update spec
func (m *DeviceAuthMiddleware) CheckUpdate(r *http.Request, uid string) bool {
user := getUserFromRequest(r)
return hasPermission(user, "devices:update")
}
// Controllers can update status (separate permission)
func (m *DeviceAuthMiddleware) CheckStatusUpdate(r *http.Request, uid string) bool {
user := getUserFromRequest(r)
// Only controllers can update status
return hasRole(user, "controller")
}
# Example RBAC rules for status subresources
# Users can update device specs
- role: user
permissions:
- devices:update
# Controllers can update device status
- role: controller
permissions:
- devices:update_status
# Admins can do both
- role: admin
permissions:
- devices:update
- devices:update_status
# Role assignments
- user: alice
role: user
- user: device-controller
role: controller
- user: bob
role: admin
Status updates publish lifecycle events with distinguishing metadata:
{
"specversion": "1.0",
"type": "io.fabrica.device.updated",
"source": "fabrica-api/resources/Device/dev-123",
"id": "evt-abc123",
"time": "2025-10-24T16:00:00Z",
"datacontenttype": "application/json",
"data": {
"action": "updated",
"resourceKind": "Device",
"resourceUID": "dev-123",
"resourceName": "sensor-01",
"resource": { ... },
"metadata": {
"updatedAt": "2025-10-24T16:00:00Z",
"updateType": "status"
}
}
}
{
"type": "io.fabrica.device.updated",
"data": {
"metadata": {
"updatedAt": "2025-10-24T16:00:00Z"
// No updateType field (or updateType: "spec")
}
}
}
// Subscribe to all device updates
eventBus.Subscribe("io.fabrica.device.updated", func(ctx context.Context, event events.Event) error {
var data events.ResourceChangeData
event.DataAs(&data)
// Check if this is a status update
if updateType, ok := data.Metadata["updateType"].(string); ok && updateType == "status" {
fmt.Printf("Status updated for device %s\n", data.ResourceUID)
} else {
fmt.Printf("Spec updated for device %s\n", data.ResourceUID)
}
return nil
})
func TestStatusSubresource(t *testing.T) {
// Create device
device := createDevice(t, DeviceSpec{
Name: "sensor-01",
Location: "dc1",
})
// User updates spec
device.Spec.Location = "dc2"
updateDevice(t, device.UID, device.Spec)
// Controller updates status
status := DeviceStatus{
Phase: "Ready",
Health: "Healthy",
}
updateDeviceStatus(t, device.UID, status)
// Verify both are updated
final := getDevice(t, device.UID)
assert.Equal(t, "dc2", final.Spec.Location)
assert.Equal(t, "Ready", final.Status.Phase)
}
status.Phase = "Ready" // ✅ Observed
status.Health = "Healthy" // ✅ Observed
status.LastSeen = time.Now() // ✅ Observed
dev.Status.Phase = "Ready"
r.UpdateStatus(ctx, dev) // ✅ Status-only update
resource.SetResourceCondition(ctx, dev,
"Ready", "True", "DeviceOnline", "Device is operational")
spec.CalculatedField = "value" // ❌ Use status
dev.Spec.Location = "new" // ❌ Spec is user-defined
// ❌ Old pattern
PUT /devices/{uid}
{
"spec": {...},
"status": {...}
}
Problem: Status update returns 404 or 405
Solution: Verify route is registered:
# Check generated routes
grep "status" cmd/server/routes_generated.go
Should show:
r.Route("/status", func(r chi.Router) {
r.Put("/", UpdateDeviceStatus)
r.Patch("/", PatchDeviceStatus)
})
Problem: Reconciler overwrites spec changes
Solution: Use BaseReconciler.UpdateStatus() which loads fresh resource:
// ❌ Don't do this
r.Client.Update(ctx, dev) // Overwrites everything
// ✅ Do this instead
r.UpdateStatus(ctx, dev) // Only updates status
Problem: Controllers can’t update status
Solution: Implement StatusPolicy interface:
type DevicePolicy struct {
// ... existing fields
}
// Add this method
func (p *DevicePolicy) CanUpdateStatus(ctx context.Context, auth interface{}, r *http.Request, uid string) PolicyDecision {
// Check if user has controller role
user := getUserFromAuth(auth)
if isController(user) {
return PolicyDecision{Allowed: true}
}
return PolicyDecision{Allowed: false, Reason: "not a controller"}
}
If you have existing code that updates both spec and status:
// Old pattern - updates everything
dev.Spec.Location = "dc2"
dev.Status.Phase = "Ready"
storage.SaveDevice(ctx, dev)
// New pattern - separate updates
// User updates (spec)
dev.Spec.Location = "dc2"
storage.SaveDevice(ctx, dev)
// Controller updates (status) - in reconciler
dev.Status.Phase = "Ready"
r.UpdateStatus(ctx, dev) // Loads fresh, preserves spec
Generated OpenAPI specs include status subresource operations:
paths:
/devices/{uid}/status:
put:
summary: Update device status
description: |
Updates only the status portion of a device. This endpoint is intended
for controllers, reconcilers, and monitoring systems.
parameters:
- name: uid
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/DeviceStatus'
responses:
'200':
description: Status updated successfully
'403':
description: Forbidden - requires update_status permission
Implementation Status: ✅ Complete Version: Fabrica 0.3.1+ Questions? GitHub Discussions