Core Concepts
Learn the fundamentals of r8s in 5 minutes
Components
r8s components are TypeScript functions that return Kubernetes resources. They follow the same pattern as React components but render infrastructure instead of DOM.
import { App } from '@r8s/recipes';
export default () => (
<App
name="api"
image="myapp/api:v1.2.3"
host="api.example.com"
/>
);The <App> component above creates a Deployment, Service, and Endpoint — all wired together with sensible defaults.
Composition
Compose multiple components together using fragments. Each component renders its own resources, and the renderer flattens everything into a single list.
import { App, Database } from '@r8s/recipes';
export default () => (
<>
<Database name="api-db" storage="10Gi" />
<App
name="api"
image="myapp/api:v1.2.3"
host="api.example.com"
/>
</>
);Escape Hatches
Prefer raw Kubernetes? Every API resource is available as a lowercase component. Drop down to raw components at any level:
import { Database, Endpoint } from '@r8s/recipes';
export default function CustomApp() {
return (
<>
<Database name="myapp-db" storage="20Gi" />
<deployment
metadata={{ name: 'myapp-web' }}
spec={{
replicas: 3,
template: {
spec: {
containers: [{
name: 'web',
image: 'myapp/web:v1.2.3',
ports: [{ containerPort: 3000 }],
}],
},
},
}}
/>
<service
metadata={{ name: 'myapp-web' }}
spec={{
selector: { app: 'myapp-web' },
ports: [{ port: 80, targetPort: 3000 }],
}}
/>
<Endpoint
host="myapp.example.com"
serviceName="myapp-web"
servicePort={80}
tls={{ secretName: "myapp-tls", clusterIssuer: "letsencrypt" }}
/>
</>
);
}Rendering
The render() function takes your component tree and returns two things: all Kubernetes resources and all required operators.
import { render } from '@r8s/core';
import { App, Database } from '@r8s/recipes';
const result = render(
<>
<Database name="app-db" storage="10Gi" />
<App name="api" image="myapp/api:v1" host="api.example.com" />
</>
);
// All Kubernetes resources
console.log(result.resources);
// [{ apiVersion: "apps/v1", kind: "Deployment", ... }, ...]
// All required operators
console.log(result.operators);
// [{ name: "cnpg", source: { type: "helm", ... } }, ...]Context
Share configuration across components using context — namespace, labels, and operators can be provided once and consumed everywhere.
import { Namespace, Labels } from '@r8s/core/defaults';
import { Database, App } from '@r8s/recipes';
export default function Platform() {
return (
<Namespace.Provider value="production">
<Labels.Provider value={{ app: 'myapp', team: 'platform' }}>
<Database name="app-db" storage="10Gi" />
<App name="api" image="myapp/api:v1" host="api.example.com" />
</Labels.Provider>
</Namespace.Provider>
);
}Providers
Providers configure cluster-level concerns: secrets, DNS, and routing. Use strings for simple cases, components for advanced configuration.
import { SecretProvider, DnsProvider, EndpointProvider, App } from '@r8s/recipes';
export default () => (
<SecretProvider provider="openbao">
<DnsProvider provider="external-dns">
<EndpointProvider provider="nginx">
<App name="api" image="myapp:v1" host="api.example.com" />
</EndpointProvider>
</DnsProvider>
</SecretProvider>
);For advanced configuration, pass a component instead of a string:
import { SecretProvider, OpenBao, DnsProvider, ExternalDns, EndpointProvider, Nginx, App } from '@r8s/recipes';
export default () => (
<SecretProvider provider={<OpenBao mount="secret" path="infra" />}>
<DnsProvider provider={<ExternalDns server="ns1.example.com" tsig={{ path: 'dns/tsig', key: 'secret' }} />}>
<EndpointProvider provider={<Nginx className="nginx-internal" />}>
<App name="api" image="myapp:v1" host="api.example.com" />
</EndpointProvider>
</DnsProvider>
</SecretProvider>
);Each provider declares its required operators automatically. The hierarchy is composable — wrap your entire cluster or a single namespace.
Ready to dive deeper?
Check out the recipes for production-ready templates, or read about deployment strategieswith FluxCD and GitHub Actions.