Supabase DO App Spec Library - TypeScript classes for DigitalOcean App Platform infrastructure with YAML serialization support. Wicked awesome, kid.
wickedbased provides type-safe, object-oriented TypeScript classes for defining DigitalOcean App Platform resources. Replace those wicked fragile YAML templates with a robust, composable API that produces clean, properly-formatted YAML output. It's gonna be wicked pissa.
- Type-safe: Full TypeScript support with comprehensive type definitions
- Validated: Built-in validation ensures all configurations are valid before serialization
- Composable: Build complex configurations from reusable components
- Pulumi Ready: Seamlessly integrates with Pulumi Outputs for runtime value resolution
- Clean YAML Output: Automatic camelCase → snake_case transformation for API compatibility
npm install wickedbased
# or
pnpm add wickedbased
# or
yarn add wickedbasedimport { Service, DockerImage, HealthCheck, EnvironmentVariable } from 'wickedbased';
// Create a service with health checks and environment variables
const service = new Service({
name: 'rest',
instanceSizeSlug: 'professional-xs',
instanceCount: 1,
httpPort: 3000,
source: new DockerImage({
registryType: 'DOCKER_HUB',
registry: 'postgrest',
repository: 'postgrest',
tag: 'v14.2'
}),
healthCheck: new HealthCheck({
httpPath: '/',
port: 3000,
initialDelaySeconds: 45
}),
envs: [
new EnvironmentVariable({
key: 'DATABASE_URL',
value: 'postgresql://user:pass@db:5432/postgres',
type: 'SECRET',
scope: 'RUN_TIME'
})
]
});
// Convert to YAML
const yaml = await service.toYAML();
console.log(yaml);Output:
name: rest
instance_size_slug: professional-xs
instance_count: 1
http_port: 3000
source:
registry_type: DOCKER_HUB
registry: postgrest
repository: postgrest
tag: v14.2
health_check:
http_path: /
port: 3000
initial_delay_seconds: 45
period_seconds: 10
timeout_seconds: 1
success_threshold: 1
failure_threshold: 3
envs:
- key: DATABASE_URL
value: postgresql://user:pass@db:5432/postgres
type: SECRET
scope: RUN_TIMERepresents a web service that receives HTTP traffic.
import { Service, DockerImage, HealthCheck } from 'wickedbased';
const service = new Service({
name: 'my-service',
instanceSizeSlug: 'professional-xs',
instanceCount: 2,
httpPort: 8080,
internalPorts: [8081, 8082],
healthCheck: new HealthCheck({
httpPath: '/health',
port: 8080,
initialDelaySeconds: 30,
periodSeconds: 10,
timeoutSeconds: 5
}),
runCommand: 'npm start',
source: new DockerImage({
registryType: 'DOCKER_HUB',
repository: 'node',
tag: '20-alpine'
})
});
const yaml = await service.toYAML();Represents a one-time or scheduled task that runs to completion.
import { Job, DockerImage } from 'wickedbased';
const preDeployJob = new Job({
name: 'migrate',
kind: 'PRE_DEPLOY',
runCommand: 'npx prisma migrate deploy',
instanceSizeSlug: 'professional-xs',
instanceCount: 1,
source: new DockerImage({
registryType: 'DOCKER_HUB',
repository: 'node',
tag: '20-alpine'
})
});
const yaml = await job.toYAML();Represents a background worker that runs continuously without HTTP traffic.
import { Worker, DockerImage } from 'wickedbased';
const worker = new Worker({
name: 'background-processor',
instanceSizeSlug: 'professional-xs',
instanceCount: 2,
runCommand: 'npm run worker',
source: new DockerImage({
registryType: 'DOCKER_HUB',
repository: 'node',
tag: '20-alpine'
})
});
const yaml = await worker.toYAML();Environment variables with type and scope support.
const envVar = new EnvironmentVariable({
key: 'DATABASE_URL',
value: 'postgresql://localhost:5432/db',
type: 'SECRET', // GENERAL | SECRET
scope: 'RUN_TIME' // RUN_TIME | BUILD_TIME | DEPLOY_TIME
});Health check configuration with customizable timing parameters.
const healthCheck = new HealthCheck({
httpPath: '/',
port: 3000,
initialDelaySeconds: 45, // Wait before first check (default: 0)
periodSeconds: 10, // Time between checks (default: 10)
timeoutSeconds: 10, // Check timeout (default: 1)
successThreshold: 1, // Successes needed (default: 1)
failureThreshold: 3 // Failures to mark unhealthy (default: 3)
});Docker image source configuration.
// Docker Hub (without registry)
const dockerHub = new DockerImage({
registryType: 'DOCKER_HUB',
repository: 'nginx',
tag: 'latest'
});
// DigitalOcean Container Registry (requires registry)
const docr = new DockerImage({
registryType: 'DOCR',
registry: 'my-registry',
repository: 'my-app',
tag: 'v1.0.0'
});
// GitHub Container Registry
const ghcr = new DockerImage({
registryType: 'GHCR',
repository: 'my-org/my-app',
tag: 'main'
});GitHub repository source configuration.
const gitHubSource = new GitHubSource({
repo: 'owner/repo',
branch: 'main',
sourceDir: '/', // Source directory (default: /)
dockerfilePath: 'Dockerfile', // Dockerfile path (default: Dockerfile)
deployOnPush: false // Auto-deploy on push (default: false)
});All resources (Service, Job, Worker) support environment variable management:
const service = new Service({
name: 'my-service',
instanceSizeSlug: 'basic-xxs',
instanceCount: 1,
envs: [
new EnvironmentVariable({
key: 'PORT',
value: '3000'
})
]
});
// Add environment variable
service.addEnv(new EnvironmentVariable({
key: 'NODE_ENV',
value: 'production',
type: 'GENERAL'
}));
// Get environment variable
const port = service.getEnv('PORT');
// Remove environment variable
service.removeEnv('NODE_ENV');
// List all environment variables
const allEnvs = service.listEnvs();Use Pulumi Outputs for runtime value resolution:
import * as pulumi from '@pulumi/pulumi';
import { Service, DockerImage, EnvironmentVariable } from 'wickedbased';
// Assume dbUrl is a Pulumi Output from a database resource
const dbUrl = pulumi.output('postgresql://user:pass@db:5432/postgres');
const service = new Service({
name: 'my-service',
instanceSizeSlug: 'basic-xxs',
instanceCount: 1,
source: new DockerImage({
registryType: 'DOCKER_HUB',
repository: 'node',
tag: '20-alpine'
}),
envs: [
new EnvironmentVariable({
key: 'DATABASE_URL',
value: dbUrl, // Pulumi Output - resolved at runtime
type: 'SECRET',
scope: 'RUN_TIME'
})
]
});
// toYAML() automatically resolves Pulumi Outputs
const yaml = await service.toYAML();All configurations are validated on construction:
import { ValidationError } from 'wickedbased';
try {
const service = new Service({
name: '', // Empty name - will throw ValidationError
instanceSizeSlug: 'basic-xxs',
instanceCount: 1
});
} catch (error) {
if (error instanceof ValidationError) {
console.error(`Validation failed for field "${error.field}": ${error.constraint}`);
// Validation failed for field "name": must be non-empty (excluding whitespace)
}
}- name: Non-empty string
- instanceSizeSlug: Valid DO instance size slug (e.g., 'basic-xxs', 'professional-xs')
- instanceCount: Positive integer
- httpPort: Integer between 1 and 65535
- envs.key: Non-empty string, unique within resource
- healthCheck.port: Integer between 1 and 65535
- healthCheck.timing: Non-negative integers (periodSeconds, timeoutSeconds must be > 0)
Common DigitalOcean App Platform instance sizes:
| Size | vCPUs | RAM | Transfer |
|---|---|---|---|
| basic-xxs | 0.125 vCPU | 256 MB | 200 GB |
| basic-xs | 0.25 vCPU | 512 MB | 500 GB |
| basic-s | 0.5 vCPU | 1 GB | 1 TB |
| basic-m | 1 vCPU | 2 GB | 2.5 TB |
| professional-xs | 1 vCPU | 2 GB | 4 TB |
| professional-s | 2 vCPU | 4 GB | 5 TB |
| professional-m | 4 vCPU | 8 GB | 6 TB |
| professional-l | 8 vCPU | 16 GB | 8 TB |
# templates/service.yaml
name: rest
instance_size_slug: professional-xs
instance_count: 1
source:
registry_type: DOCKER_HUB
repository: postgrest
tag: v14.2
http_port: 3000
health_check:
http_path: /
port: 3000
initial_delay_seconds: 45
envs:
- key: DATABASE_URL
value: "{{ .Values.database.url }}"
type: SECRET
scope: RUN_TIMEimport { Service, DockerImage, HealthCheck, EnvironmentVariable } from 'wickedbased';
const service = new Service({
name: 'rest',
instanceSizeSlug: 'professional-xs',
instanceCount: 1,
source: new DockerImage({
registryType: 'DOCKER_HUB',
repository: 'postgrest',
tag: 'v14.2'
}),
httpPort: 3000,
healthCheck: new HealthCheck({
httpPath: '/',
port: 3000,
initialDelaySeconds: 45
}),
envs: [
new EnvironmentVariable({
key: 'DATABASE_URL',
value: databaseUrl, // Direct value or Pulumi Output
type: 'SECRET',
scope: 'RUN_TIME'
})
]
});
const yaml = await service.toYAML();import { Service, DockerImage, GitHubSource } from 'wickedbased';
const apiService = new Service({
name: 'api',
instanceSizeSlug: 'professional-xs',
instanceCount: 2,
httpPort: 8080,
source: new GitHubSource({
repo: 'org/api',
branch: 'main'
})
});
const webService = new Service({
name: 'web',
instanceSizeSlug: 'basic-s',
instanceCount: 1,
httpPort: 80,
source: new DockerImage({
registryType: 'DOCKER_HUB',
repository: 'nginx',
tag: 'latest'
})
});
const apiYaml = await apiService.toYAML();
const webYaml = await webService.toYAML();import { Job, DockerImage } from 'wickedbased';
const preDeployJob = new Job({
name: 'migrate',
kind: 'PRE_DEPLOY',
runCommand: 'npx prisma migrate deploy',
instanceSizeSlug: 'professional-xs',
instanceCount: 1,
source: new DockerImage({
registryType: 'DOCKER_HUB',
repository: 'node',
tag: '20-alpine'
})
});
const postDeployJob = new Job({
name: 'seed',
kind: 'POST_DEPLOY',
runCommand: 'npm run seed',
instanceSizeSlug: 'basic-xxs',
instanceCount: 1,
source: new DockerImage({
registryType: 'DOCKER_HUB',
repository: 'node',
tag: '20-alpine'
})
});import { Service, DockerImage, VolumeMount } from 'wickedbased';
const service = new Service({
name: 'data-service',
instanceSizeSlug: 'basic-m',
instanceCount: 1,
source: new DockerImage({
registryType: 'DOCKER_HUB',
repository: 'node',
tag: '20-alpine'
}),
volumes: [
new VolumeMount({
name: 'data',
mountPath: '/data',
size: 10 // Size in GB
})
]
});Service- Web service resourceJob- One-time task resourceWorker- Background worker resourceEnvironmentVariable- Environment variable configurationHealthCheck- Health check configurationDockerImage- Docker image sourceGitHubSource- GitHub repository sourceVolumeMount- Volume mount configuration
abstract class AppPlatformResource {
readonly name: string;
addEnv(env: EnvironmentVariable): void;
removeEnv(key: string): boolean;
getEnv(key: string): EnvironmentVariable | undefined;
listEnvs(): readonly EnvironmentVariable[];
abstract toJSON(): Record<string, unknown>;
async toYAML(): Promise<string>;
}class Service extends AppPlatformResource {
constructor(config: {
name: string;
instanceSizeSlug: string;
instanceCount: number;
httpPort?: number;
internalPorts?: number[];
healthCheck?: HealthCheck;
runCommand?: string;
volumes?: VolumeMount[];
source?: DockerImage | GitHubSource;
envs?: EnvironmentVariable[];
});
}class Job extends AppPlatformResource {
constructor(config: {
name: string;
kind: 'PRE_DEPLOY' | 'POST_DEPLOY';
runCommand: string;
instanceSizeSlug?: string;
instanceCount?: number;
source?: DockerImage | GitHubSource;
envs?: EnvironmentVariable[];
});
}class Worker extends AppPlatformResource {
constructor(config: {
name: string;
instanceSizeSlug: string;
instanceCount: number;
internalPorts?: number[];
runCommand?: string;
volumes?: VolumeMount[];
source?: DockerImage | GitHubSource;
envs?: EnvironmentVariable[];
});
}All validation errors throw ValidationError:
import { ValidationError } from 'wickedbased';
try {
const service = new Service({
name: 'test',
instanceSizeSlug: 'basic-xxs',
instanceCount: -1 // Invalid
});
} catch (error) {
if (error instanceof ValidationError) {
console.error(error.field); // 'instanceCount'
console.error(error.value); // -1
console.error(error.constraint); // 'must be a positive integer'
}
}Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
MIT
- GitHub Issues: https://github.com/your-org/wickedbased/issues
- Documentation: https://github.com/your-org/wickedbased/tree/main/packages/wickedbased
See CHANGELOG.md for version history.