Skip to content

External References ​

External references coordinate between different compositions, enabling multi-composition architectures.

When to Use ​

ScenarioSolution
Resources in the same compositionDirect references (magic proxy)
Resources in different compositionsexternalRef()
Cross-team shared servicesexternalRef() with shared type definitions

Quick Start ​

typescript
import { externalRef, kubernetesComposition } from 'typekro';
import { Deployment } from 'typekro/simple';

// Define types for the external resource
interface DatabaseSpec { name: string; }
interface DatabaseStatus { ready: boolean; host: string; port: number; }

// Reference a resource from another composition
const dbRef = externalRef<DatabaseSpec, DatabaseStatus>(
  'database.example.com/v1alpha1',  // API version
  'Database',                       // Kind
  'my-postgres',                    // Instance name
  'default'                         // Namespace (optional)
);

// Use it in a composition
const app = kubernetesComposition(definition, (spec) => {
  const deploy = Deployment({
    id: 'app',
    name: spec.name,
    image: spec.image,
    env: {
      DATABASE_HOST: dbRef.status.host,
      DATABASE_READY: dbRef.status.ready
    }
  });
  
  return { ready: deploy.status.readyReplicas > 0 };
});

Type Safety ​

External references maintain full type safety with proper type parameters:

typescript
interface DatabaseStatus {
  ready: boolean;
  host: string;
  port: number;
}

const dbRef = externalRef<DatabaseSpec, DatabaseStatus>(
  'database.example.com/v1alpha1',
  'Database',
  'my-postgres'
);

// ✅ Type-safe access
dbRef.status.host;    // string
dbRef.status.ready;   // boolean

// ❌ Compile-time errors
dbRef.status.invalid; // Property doesn't exist

Real-World Example ​

typescript
import { type } from 'arktype';
import { kubernetesComposition, externalRef } from 'typekro';
import { Deployment, Service } from 'typekro/simple';

// Types for the external database resource
interface DatabaseSpec { name: string; storage: string; }
interface DatabaseStatus { ready: boolean; host: string; }

// Database composition (deployed separately)
const database = kubernetesComposition(dbDefinition, (spec) => {
  const postgres = Deployment({ id: 'db', name: 'postgres', image: 'postgres:15' });
  const service = Service({
    id: 'svc',
    name: 'postgres-service',
    selector: { app: 'postgres' },
    ports: [{ port: 5432 }]
  });
  
  return {
    ready: postgres.status.readyReplicas > 0,
    host: service.status.clusterIP
  };
});

// Application composition (references database)
const application = kubernetesComposition(appDefinition, (spec) => {
  const dbRef = externalRef<DatabaseSpec, DatabaseStatus>(
    'data.company.com/v1alpha1',
    'Database',
    'main-database'
  );
  
  const app = Deployment({
    id: 'app',
    name: spec.name,
    image: spec.image,
    env: {
      DATABASE_HOST: dbRef.status.host,
      DATABASE_READY: dbRef.status.ready
    }
  });
  
  return {
    ready: app.status.readyReplicas > 0 && dbRef.status.ready,
    databaseConnected: dbRef.status.ready
  };
});

Deployment Order ​

Deploy compositions in dependency order:

typescript
// 1. Deploy database first
await databaseFactory.deploy({ name: 'main-database', storage: '50Gi' });

// 2. Deploy app that references it
await appFactory.deploy({ name: 'my-app', image: 'nginx' });

Observing a Resource Your Own Composition Creates ​

A composition often needs to read a resource it does not author: the Service a Helm chart creates, or a CRD instance an operator produces. observedResource() reads that resource live rather than applying it, so on a fresh deployment it does not exist yet.

Declare dependsOn() on the observed resource, naming the resource in the same graph whose deployment produces it:

typescript
import { kubernetesComposition, observedResource } from 'typekro';

const platform = kubernetesComposition(definition, (spec) => {
  const release = helmRelease({
    id: 'gatewayRelease',
    name: spec.name,
    chart: { name: 'gateway', version: '1.4.0' },
    // ...
  });

  // The chart creates this Service. Without dependsOn, the read happens before the
  // release is applied and fails with a 404.
  const gatewayService = observedResource<ServiceSpec, ServiceStatus>({
    apiVersion: 'v1',
    kind: 'Service',
    metadata: { name: `${spec.name}-gateway`, namespace: spec.namespace },
    id: 'gatewayService',
  }).dependsOn(release);

  return {
    ready: release.status.conditions.some((c) => c.type === 'Ready' && c.status === 'True'),
    address: gatewayService.status.loadBalancer.ingress[0].ip,
  };
});

With dependsOn, the deployment applies the release, waits for it to become ready, then reads the Service — retrying until it appears or the read budget runs out. Anything that consumes the observed resource is scheduled behind that read.

If the resource never appears, the deployment fails with an error naming the reference, its dependsOn targets, and how long it waited. It is never silently skipped.

Only a failure that waiting could fix is retried: a 404 for the object itself, a 429, a 5xx, or a transport failure. A read that is rejected — RBAC (401/403), a malformed reference (400/405/422), or an apiVersion/kind the cluster does not serve — fails the deployment on the first attempt, with the classification and the API server's own message, rather than polling to the end of the budget and reporting a timeout:

Required external resource Service/my-app-gateway (reference 'gatewayService') could not
be read: permission denied (HTTP 403). Waiting cannot fix this, so the deployment failed
immediately instead of polling its dependsOn targets [gatewayRelease]: services
"my-app-gateway" is forbidden: User "deployer" cannot get resource "services"

An observed resource with no dependsOn on an in-graph resource is still read before anything is applied, which is the right behaviour for a resource another composition or another team already owns.

Cross-Namespace References ​

Reference resources in different namespaces:

typescript
const dbRef = externalRef<DatabaseSpec, DatabaseStatus>(
  'data.company.com/v1alpha1',
  'Database',
  'shared-postgres',
  'shared-services'  // Different namespace
);

Best Practices ​

Use Shared Type Definitions ​

typescript
// shared-types/platform.ts
export interface PlatformDatabaseStatus {
  ready: boolean;
  connectionString: string;
  primaryHost: string;
}

// app-service/composition.ts
import { PlatformDatabaseStatus } from '../shared-types/platform.js';

const dbRef = externalRef<any, PlatformDatabaseStatus>(
  'data.platform.company.com/v1', 'Database', 'shared-postgres'
);

Clear Naming Conventions ​

typescript
// ✅ Good: Clear, predictable naming
const dbRef = externalRef<DatabaseSpec, DatabaseStatus>(
  'data.platform.company.com/v1',
  'Database',
  `${spec.environment}-postgres`,  // environment-service pattern
  `data-${spec.environment}`       // namespace pattern
);

Debugging ​

bash
# Check if external resource exists
kubectl get database main-database -o yaml

# Check status
kubectl get database main-database -o jsonpath='{.status}'

# Check RBAC permissions
kubectl auth can-i get database --as=system:serviceaccount:default:typekro

Next Steps ​

Released under the Apache 2.0 License.