Developer Guide

This guide is for application developers who use the App Platform to deploy and manage applications. It covers the dashboard, GitOps workflow, secrets, OIDC, and day-to-day operations.

Dashboard Overview

The App Platform dashboard is your primary interface for managing resources. After logging in via SSO, you'll see:

  • App Registrations (admin only) — AppPlatformRegistrations and their status
  • Git Repositories — Flux GitRepository sources being watched
  • Kustomizations — Flux Kustomization syncs applying manifests
  • Namespaces — App namespaces you can create and manage

Resources marked with a "Managed" badge are created by the operator and are read-only in the dashboard. You cannot edit or delete these — they are controlled by the AppPlatformRegistration lifecycle.

GitOps Workflow

All deployments use GitOps via Flux. You never kubectl apply directly. Instead:

1. Push Kubernetes manifests to your app repo
2. Use the dashboard to create GitRepository + Kustomization CRs
3. Flux detects changes in your app repo (within 1-5 minutes)
4. Flux applies the manifests to the cluster
5. Dashboard shows updated resource status

Your App Repo

Your application repository (e.g., apps.my-app) contains both your application source code and Kubernetes manifests:

apps.my-app/
├── src/                         # Application source code
├── Dockerfile
└── k8s/
    ├── base/
    │   ├── kustomization.yaml
    │   ├── deployment.yaml
    │   ├── service.yaml
    │   └── ingress.yaml
    └── overlays/
        ├── dev/
        │   └── kustomization.yaml
        └── production/
            └── kustomization.yaml

Your Platform Repo

When your app is registered, the operator creates a private GitHub repo named <app>-platform. This repo is managed by the operator — it contains Flux sync configuration and SOPS encryption config. You do not push manifests to this repo directly.

The operator auto-commits:

FilePurpose
.sops.yamlAGE encryption config for SOPS secrets
README.mdAuto-generated documentation

Setting Up Flux Sync via the Dashboard

Use the dashboard to tell Flux where your manifests are:

  1. Create a GitRepository — Under Git Repositories > Create, point it at your app repo's SSH URL. This tells Flux's Source Controller to watch your repo.
  1. Create a Kustomization — Under Kustomizations > Create, reference the GitRepository and set the path to your overlay directory (e.g., k8s/overlays/production). This tells Flux to apply the manifests at that path.
  1. Push to your app repo — Every git push is automatically deployed within 1-5 minutes.

> Note: The operator auto-creates a GitRepository and Kustomization for the platform repo itself (shown with the "Managed" badge). You create additional ones for your app repo.

Deploying Changes

# Edit manifests in your app repo
cd apps.my-app
vim k8s/base/deployment.yaml

# Commit and push
git add -A && git commit -m "feat: update replica count" && git push

# Flux applies within 1-5 minutes
# Check status in the dashboard

Managing Namespaces

App namespaces are declared in the AppPlatformRegistration CRD under spec.namespaces. You manage them from the dashboard:

  1. Navigate to Namespaces in the sidebar
  2. Select your app using the context selector
  3. Click Add Namespace and enter a name (must start with <app-name>-, e.g., my-app-prod)
  4. To remove a namespace, click Remove next to it

When you add a namespace, the operator automatically:

  • Creates the Kubernetes namespace with labels (labrats.work/app, labrats.work/namespace-role: app)
  • Creates RBAC RoleBindings based on spec.access entries
  • Grants the namespace access to read platform secrets (secret-reader RBAC)

When you remove a namespace, the operator removes the RBAC bindings but does not delete the Kubernetes namespace itself.

> Note: All namespace names must start with your app name (e.g., my-app- prefix). Platform namespaces (<app>-platform) are managed by the operator automatically and are not included in spec.namespaces.

Working with Secrets

Secrets are managed via the dashboard. You never need to clone the platform repo or run sops manually.

Creating a Secret

  1. Navigate to Secrets in the sidebar
  2. Select your app using the context selector
  3. Click Create Secret
  4. Enter a name, select one or more target namespaces, and add key-value pairs
  5. Click Create

The operator automatically:

  • Encrypts the secret data using SOPS + AGE
  • Commits the encrypted secret to the platform Git repo
  • Flux detects the change and deploys the decrypted secret to the target namespace(s)

The secret status will show Pending until the operator commits it, then Committed.

Updating a Secret

  1. Navigate to Secrets and click Edit on the secret
  2. Enter new values for existing keys or add new keys
  3. Click Update — the operator re-encrypts and commits the change

Deleting a Secret

Click Delete on a secret. The operator removes the encrypted file from Git and deletes the Kubernetes Secret.

How It Works

Dashboard → K8s Secret (intent) → Operator (encrypt + commit) → Flux (decrypt + apply)
  • Secrets are write-only — the dashboard shows key names but never values
  • Each secret can target multiple namespaces (e.g., both my-app-dev and my-app-prod)
  • Only admins can create, update, or delete secrets; all roles can view key names

OIDC / SSO Integration

OIDC clients are registered by creating a labeled Secret in the <app>-platform namespace. The OidcClientReconciler watches these Secrets, generates the client secret, and registers the client with Authelia.

apiVersion: v1
kind: Secret
metadata:
  name: my-app-oidc
  namespace: my-app-platform
  labels:
    labrats.work/oidc-client: my-app           # client_id in Authelia
    labrats.work/app: my-app
type: Opaque
stringData:
  hostname: app.my-app.hcl.labrats.work
  callbackPath: /api/auth/oidc/callback        # optional
  # clientSecret is operator-generated

Multiple OIDC clients per app are supported — one Secret per client/hostname. Status is reported via the labrats.work/oidc-status annotation on the Secret.

What You Get

ResourceLocation
Client IDThe labrats.work/oidc-client label value
Client SecretclientSecret key in the labeled Secret (operator-generated)
Issuer URLhttps://my-id.hcl.labrats.work
Callback URLhttps://<hostname><callbackPath>

Using OIDC in Your App

Your app needs to implement the OAuth2 Authorization Code flow:

  1. Redirect users to Authelia's authorize endpoint
  2. Handle the callback at your callbackPath (default: /api/auth/oidc/callback)
  3. Exchange the authorization code for tokens
  4. Extract user info from the ID token

The client secret is stored as the clientSecret key on the labeled Secret in the <app>-platform namespace. To use it in your app namespace, set up cross-namespace access using the External Secrets Operator:

  1. Create a ServiceAccount named secret-reader in your app namespace
  2. Create a SecretStore referencing the platform namespace
  3. Create an ExternalSecret that pulls clientSecret from the labeled OIDC Secret

The operator automatically creates the RBAC (Role + RoleBinding) in the platform namespace to allow the secret-reader ServiceAccount to read secrets. This RBAC is managed as part of the spec.namespaces reconciliation.

Access Roles

Your access is determined by group membership in Authelia (lldap). The operator binds groups to custom app-platform-* ClusterRoles:

RoleClusterRoleCapabilities
adminapp-platform-adminFull CRUD on workloads, secrets, services, ingresses, jobs; create/delete namespaces
developerapp-platform-developerRead workloads, secrets, services; exec into pods, port-forward
readerapp-platform-readerRead-only access to workloads, services, and pod logs

Contact your platform admin to update group membership.

Ingress & TLS

All apps use Traefik ingress with automatic TLS via cert-manager:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
  namespace: my-app
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  rules:
    - host: my-app.hcl.labrats.work
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app
                port:
                  number: 80
  tls:
    - hosts:
        - my-app.hcl.labrats.work
      secretName: my-app-tls

The domain pattern is <app>.hcl.labrats.work.

Container Best Practices

Follow these conventions for containers on the platform:

  • Non-root: Run as uid 1001 (securityContext.runAsUser: 1001)
  • Read-only filesystem: Set readOnlyRootFilesystem: true where possible
  • Resource limits: Always set CPU/memory requests and limits
  • Image tags: Use semantic versions (e.g., 1.2.3), never latest
  • Registry: Use ghcr.io/labrats-work/<image> for org images

Troubleshooting

My push didn't deploy

  1. Check GitRepository: is the source syncing?
  2. Check Kustomization: is it reconciling without errors?
  3. View conditions in the dashboard for error messages

Secret stuck in "Pending" status

  • Verify the AppPlatformRegistration is Ready (the operator needs GitHub access to commit)
  • Check that the platform namespace (<app>-platform) exists
  • Check operator logs for encryption or commit errors

SOPS decryption failed

  • The operator encrypts secrets automatically — do not manually modify files in the platform repo
  • Check that the AGE key secret exists in the platform namespace
  • If corruption occurs, delete and recreate the secret via the dashboard

Can't see my resources in the dashboard

  • Verify your Authelia group matches the spec.access entries
  • Check with your platform admin that the AppPlatformRegistration is Ready

Kustomization shows "dependency not ready"

Your Flux Kustomization may depend on another resource. Check spec.dependsOn and ensure the dependency is healthy.