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:
| File | Purpose |
|---|---|
.sops.yaml | AGE encryption config for SOPS secrets |
README.md | Auto-generated documentation |
Setting Up Flux Sync via the Dashboard
Use the dashboard to tell Flux where your manifests are:
- 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.
- Create a Kustomization — Under Kustomizations > Create, reference the GitRepository and set the
pathto your overlay directory (e.g.,k8s/overlays/production). This tells Flux to apply the manifests at that path.
- Push to your app repo — Every
git pushis 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:
- Navigate to Namespaces in the sidebar
- Select your app using the context selector
- Click Add Namespace and enter a name (must start with
<app-name>-, e.g.,my-app-prod) - 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.accessentries - 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
- Navigate to Secrets in the sidebar
- Select your app using the context selector
- Click Create Secret
- Enter a name, select one or more target namespaces, and add key-value pairs
- 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
- Navigate to Secrets and click Edit on the secret
- Enter new values for existing keys or add new keys
- 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-devandmy-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
| Resource | Location |
|---|---|
| Client ID | The labrats.work/oidc-client label value |
| Client Secret | clientSecret key in the labeled Secret (operator-generated) |
| Issuer URL | https://my-id.hcl.labrats.work |
| Callback URL | https://<hostname><callbackPath> |
Using OIDC in Your App
Your app needs to implement the OAuth2 Authorization Code flow:
- Redirect users to Authelia's authorize endpoint
- Handle the callback at your
callbackPath(default:/api/auth/oidc/callback) - Exchange the authorization code for tokens
- 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:
- Create a
ServiceAccountnamedsecret-readerin your app namespace - Create a
SecretStorereferencing the platform namespace - Create an
ExternalSecretthat pullsclientSecretfrom 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:
| Role | ClusterRole | Capabilities |
|---|---|---|
| admin | app-platform-admin | Full CRUD on workloads, secrets, services, ingresses, jobs; create/delete namespaces |
| developer | app-platform-developer | Read workloads, secrets, services; exec into pods, port-forward |
| reader | app-platform-reader | Read-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: truewhere possible - Resource limits: Always set CPU/memory requests and limits
- Image tags: Use semantic versions (e.g.,
1.2.3), neverlatest - Registry: Use
ghcr.io/labrats-work/<image>for org images
Troubleshooting
My push didn't deploy
- Check GitRepository: is the source syncing?
- Check Kustomization: is it reconciling without errors?
- 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.accessentries - 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.