This guide explains how to integrate your Deno/TypeScript application with the Deco CMS Operator to support configuration reloading.
The operator will:
- Mount
decofile.jsonandtimestamp.txtto/app/decofile/(configurable) - Set
DECO_RELEASEenvironment variable pointing to the file - Call
/.decofile/reload?timestamp=<ts>&tsFile=<path>when config changes - Long-poll until your app confirms it has the updated timestamp
Request Body (JSON):
{
"timestamp": "1731598481",
"source": "operator",
"decofile": {
"config": {"environment": "production"},
"data": {"message": "hello"}
}
}Behavior:
- Receive POST notification with decofile content
- Apply configuration to your application
- Return 200 OK on success
- Return 500 on error
Note: The decofile content is sent in the payload, so you don't need to read from disk. However, the files are still mounted for initial load and fallback.
// config.ts - Configuration loader with compression support
export interface DecofileConfig {
[key: string]: unknown;
}
export async function loadConfig(basePath?: string): Promise<DecofileConfig> {
const configDir = basePath || Deno.env.get("DECO_RELEASE")?.replace("file://", "").replace("/decofile.json", "");
if (!configDir) {
throw new Error("No config path specified and DECO_RELEASE not set");
}
// Check if compressed (.bin file exists)
try {
const binContent = await Deno.readTextFile(`${configDir}/decofile.bin`);
// File exists and is compressed - decompress it
const compressed = Uint8Array.from(atob(binContent), c => c.charCodeAt(0));
const decompressed = await decompressBrotli(compressed);
return JSON.parse(decompressed);
} catch (error) {
// .bin doesn't exist or error reading - try .json
if (error.name !== "NotFound") {
console.warn("Error reading compressed config, falling back to json:", error);
}
}
// Read uncompressed .json
const content = await Deno.readTextFile(`${configDir}/decofile.json`);
return JSON.parse(content);
}
// Brotli decompression using DecompressionStream
async function decompressBrotli(data: Uint8Array): Promise<string> {
const stream = new DecompressionStream("deflate-raw"); // Note: Browser API, or use npm:brotli-wasm
const blob = new Blob([data]);
const decompressedStream = blob.stream().pipeThrough(stream);
const decompressedBlob = await new Response(decompressedStream).blob();
return await decompressedBlob.text();
// Alternative: Use brotli-wasm for better compatibility
// import { decompress } from "https://deno.land/x/brotli/mod.ts";
// return new TextDecoder().decode(decompress(data));
}
// reload.ts - Reload endpoint handler
async function handleReload(req: Request): Promise<Response> {
console.log("=== RELOAD REQUEST ===");
console.log(`Timestamp: ${new Date().toISOString()}`);
// Parse JSON payload
try {
const payload = await req.json();
console.log(`📦 Received notification:`, payload);
console.log(` Timestamp: ${payload.timestamp}`);
} catch {
console.log(" No payload (optional)");
}
try {
// Reload configuration from mounted files
const config = await loadConfig();
const fileCount = Object.keys(config).length;
console.log(`✓ Loaded ${fileCount} config files`);
// TODO: Apply configuration to your application
// - Update in-memory state
// - Refresh caches
// - Reload components
// etc.
console.log("=== RELOAD COMPLETE ===\n");
return new Response(`Reloaded ${fileCount} files\n`, { status: 200 });
} catch (error) {
console.error(`Error reloading: ${error.message}`);
return new Response(`Error: ${error.message}\n`, { status: 500 });
}
}
// server.ts - HTTP server
Deno.serve({ port: 8000 }, async (req) => {
const url = new URL(req.url);
if (url.pathname === "/.decofile/reload") {
return await handleReload(req);
}
if (url.pathname === "/health") {
return new Response("OK\n", { status: 200 });
}
// ... your other routes
});Your application receives:
// DECO_RELEASE points to the config file
const configPath = Deno.env.get("DECO_RELEASE");
// Example: "file:///app/decofile/decofile.json"
// Parse the file path
const filePath = configPath?.replace("file://", "");
// Example: "/app/decofile/decofile.json"Mounted at /app/decofile/ (or custom path):
/app/decofile/
├── decofile.json # Your configuration
└── timestamp.txt # Update timestamp
For configs < 2.5MB (uncompressed):
{
"config": {
"environment": "production",
"apiUrl": "https://api.example.com"
},
"data": {
"message": "Hello",
"version": "1.0"
},
"Campaign Timer - 01": {
"link": {"href": "...", "text": "..."}
}
}For large configs >= 2.5MB:
The operator automatically compresses with Brotli and stores as decofile.bin (base64-encoded).
Your app should check for _compressed flag and decompress if needed (see example below).
Notes:
- Keys have
.jsonextension stripped - Filenames are URL-decoded (spaces, not
%20) - HTML characters not escaped (
&,<,>) - Large configs auto-compressed with Brotli
1731598481
Unix timestamp in seconds since epoch (UTC)
// main.ts
import { serve } from "https://deno.land/std@0.208.0/http/server.ts";
// Configuration state
let config: Record<string, unknown> = {};
// Load initial config
async function loadConfig(): Promise<void> {
const configPath = Deno.env.get("DECO_RELEASE")?.replace("file://", "");
if (!configPath) {
throw new Error("DECO_RELEASE not set");
}
const content = await Deno.readTextFile(configPath);
config = JSON.parse(content);
console.log(`✓ Loaded ${Object.keys(config).length} config files`);
}
// Reload endpoint with long-polling
async function handleReload(req: Request): Promise<Response> {
const url = new URL(req.url);
const expectedTimestamp = url.searchParams.get("timestamp");
const tsFilePath = url.searchParams.get("tsFile");
console.log("=== RELOAD REQUEST ===");
// Long-poll if timestamp provided
if (expectedTimestamp && tsFilePath) {
console.log(`Waiting for timestamp: ${expectedTimestamp}`);
const maxWait = 120_000; // 120 seconds
const pollInterval = 2000; // 2 seconds
const start = Date.now();
while (Date.now() - start < maxWait) {
try {
const fileTsStr = (await Deno.readTextFile(tsFilePath)).trim();
const fileTs = parseInt(fileTsStr, 10);
const expectedTs = parseInt(expectedTimestamp, 10);
if (fileTs >= expectedTs) {
console.log(`✓ Timestamp satisfied: ${fileTs} >= ${expectedTs}`);
break;
}
await new Promise(r => setTimeout(r, pollInterval));
} catch {
await new Promise(r => setTimeout(r, pollInterval));
}
}
}
// Reload config
await loadConfig();
// Apply changes to your app
// - Clear caches
// - Update state
// - Refresh components
console.log("=== RELOAD COMPLETE ===");
return new Response("OK\n", { status: 200 });
}
// Server
serve(async (req) => {
const url = new URL(req.url);
if (url.pathname === "/.decofile/reload") {
return handleReload(req);
}
if (url.pathname === "/health") {
return new Response("OK\n", { status: 200 });
}
// Your app logic here
return new Response("Not Found\n", { status: 404 });
}, { port: 8000 });
// Load initial config on startup
await loadConfig();
console.log("✅ Application started");async function applyConfig(newConfig: Record<string, unknown>): Promise<void> {
// Validate config first
validateConfig(newConfig);
// Apply atomically
const oldConfig = config;
try {
config = newConfig;
// Refresh dependent systems
} catch (error) {
// Rollback on failure
config = oldConfig;
throw error;
}
}function handleHealth(): Response {
// Check if config is loaded
if (Object.keys(config).length === 0) {
return new Response("Config not loaded\n", { status: 503 });
}
return new Response("OK\n", { status: 200 });
}try {
await loadConfig();
} catch (error) {
console.error("Failed to load config:", error);
// Use default config or exit
Deno.exit(1);
}// Set env var
Deno.env.set("DECO_RELEASE", "file:///app/decofile/decofile.json");
// Create test files
await Deno.writeTextFile("/app/decofile/decofile.json", JSON.stringify({
config: { environment: "test" },
data: { message: "hello" }
}));
await Deno.writeTextFile("/app/decofile/timestamp.txt", new Date().toISOString());
// Test reload
const response = await fetch("http://localhost:8000/.decofile/reload?timestamp=" +
encodeURIComponent(new Date().toISOString()) +
"&tsFile=/app/decofile/timestamp.txt"
);
console.log(await response.text()); // "OK"Your Knative Service needs the annotation:
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
annotations:
deco.sites/decofile-inject: "default" # or specific decofile name
spec:
template:
spec:
containers:
- name: app
image: your-app:latest
ports:
- containerPort: 8000 # Operator detects this portThe operator automatically:
- ✅ Mounts
/app/decofile/decofile.json - ✅ Mounts
/app/decofile/timestamp.txt - ✅ Sets
DECO_RELEASE=file:///app/decofile/decofile.json - ✅ Labels pod with
deco.sites/decofile: <name> - ✅ Calls
/.decofile/reloadon config changes
# Check pod labels
kubectl get pods -n your-namespace -l deco.sites/decofile=your-decofile
# Check operator logs
kubectl logs -n operator-system -l control-plane=controller-manager -f# Check ConfigMap
kubectl get configmap decofile-your-decofile -n your-namespace -o yaml
# Check mounted files in pod
kubectl exec -n your-namespace your-pod -- cat /app/decofile/timestamp.txt- Increase max wait time in your app
- Check kubelet sync interval
- Verify file system permissions
See types/decofile.ts for complete type definitions:
import type { DecofileJSON, DecofileEnv } from "https://raw.githubusercontent.com/decocms/operator/main/types/decofile.ts";
const config: DecofileJSON = await loadConfig();- GitHub Issues: https://github.com/decocms/operator/issues
- Documentation: https://github.com/decocms/operator
- Examples: See
test/kind/app/main.tsfor reference implementation