safety prevents schema drift, and the ecosystem provides mature HTTP, cron, and database libraries.
- State Management:
better-sqlite3 for file-based, synchronous state tracking. SQLite eliminates external dependencies, supports atomic transactions, and enables deterministic idempotency checks.
- Scheduling:
node-cron for local execution, GitHub Actions as a cloud fallback. This dual approach ensures workflows run even if the local machine is offline, without vendor lock-in.
- HTTP Layer:
axios with custom interceptors for exponential backoff, circuit breaking, and idempotency key injection.
- Configuration:
dotenv + zod for runtime validation. Silent config failures are the leading cause of automation drift.
- Observability:
pino for structured JSON logging. Logs are machine-parseable, enabling automated alerting and post-mortem analysis.
Step-by-Step Implementation
Step 1: Project Initialization and Dependency Management
mkdir solopreneur-automation && cd solopreneur-automation
npm init -y
npm install typescript tsx zod better-sqlite3 axios pino node-cron dotenv
npm i -D @types/better-sqlite3 @types/node-cron
npx tsc --init
Step 2: Configuration and Validation Layer
// src/config.ts
import { config } from 'dotenv';
import { z } from 'zod';
config();
const ConfigSchema = z.object({
DATABASE_PATH: z.string().default('./data/automation.db'),
LOG_LEVEL: z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']).default('info'),
GITHUB_TOKEN: z.string().optional(),
WEBHOOK_SECRET: z.string().min(16),
MAX_RETRIES: z.coerce.number().int().positive().default(3),
RETRY_BASE_DELAY_MS: z.coerce.number().int().positive().default(1000),
});
export const config = ConfigSchema.parse(process.env);
Step 3: Idempotency Manager with SQLite
// src/idempotency.ts
import Database from 'better-sqlite3';
import { config } from './config.js';
const db = new Database(config.DATABASE_PATH);
db.exec(`
CREATE TABLE IF NOT EXISTS execution_log (
idempotency_key TEXT PRIMARY KEY,
workflow_name TEXT NOT NULL,
payload_hash TEXT NOT NULL,
status TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
)
`);
export function isDuplicate(key: string): boolean {
const stmt = db.prepare('SELECT id FROM execution_log WHERE idempotency_key = ?');
return !!stmt.get(key);
}
export function markExecuted(key: string, workflow: string, payloadHash: string, status: string) {
const stmt = db.prepare(
'INSERT OR IGNORE INTO execution_log (idempotency_key, workflow_name, payload_hash, status) VALUES (?, ?, ?, ?)'
);
stmt.run(key, workflow, payloadHash, status);
}
export function cleanupOldLogs(days: number = 30) {
const cutoff = new Date(Date.now() - days * 86400000).toISOString();
db.prepare('DELETE FROM execution_log WHERE created_at < ?').run(cutoff);
}
Step 4: Retry-Aware HTTP Client
// src/http.ts
import axios, { AxiosInstance, AxiosError } from 'axios';
import { config } from './config.js';
import { createHash } from 'crypto';
const client: AxiosInstance = axios.create({ timeout: 10000 });
client.interceptors.response.use(
(res) => res,
async (err: AxiosError) => {
const config = err.config!;
if (!config.retryCount) config.retryCount = 0;
const isRetryable = err.response?.status === 429 || err.response?.status >= 500;
if (isRetryable && config.retryCount < config.MAX_RETRIES || 3)) {
config.retryCount++;
const delay = Math.min(
(config.retryCount || 1) * config.RETRY_BASE_DELAY_MS,
30000
);
await new Promise((r) => setTimeout(r, delay));
return client(config);
}
return Promise.reject(err);
}
);
export function generatePayloadHash(payload: unknown): string {
return createHash('sha256').update(JSON.stringify(payload)).digest('hex');
}
export { client };
Step 5: Scheduler and Workflow Registry
// src/registry.ts
import cron from 'node-cron';
import { logger } from './logger.js';
type WorkflowFn = () => Promise<void>;
const workflows: Map<string, { fn: WorkflowFn; schedule: string; enabled: boolean }> = new Map();
export function registerWorkflow(name: string, schedule: string, fn: WorkflowFn, enabled = true) {
workflows.set(name, { fn, schedule, enabled });
}
export function startScheduler() {
for (const [name, { fn, schedule, enabled }] of workflows) {
if (!enabled) continue;
if (!cron.validate(schedule)) {
logger.error({ workflow: name, schedule }, 'Invalid cron expression');
continue;
}
cron.schedule(schedule, async () => {
logger.info({ workflow: name }, 'Executing scheduled workflow');
try {
await fn();
logger.info({ workflow: name }, 'Workflow completed successfully');
} catch (err) {
logger.error({ workflow: name, error: err }, 'Workflow execution failed');
}
});
}
logger.info('Scheduler started');
}
Step 6: Entry Point
// src/index.ts
import { registerWorkflow, startScheduler } from './registry.js';
import { logger } from './logger.js';
import { cleanupOldLogs } from './idempotency.js';
// Example workflow
registerWorkflow('daily-invoice-sync', '0 9 * * *', async () => {
logger.info('Syncing invoices from accounting API');
// Implementation: fetch, transform, upsert, notify
});
registerWorkflow('cleanup-logs', '0 0 * * 0', async () => {
cleanupOldLogs(30);
});
startScheduler();
logger.info('Automation engine initialized');
Step 7: GitHub Actions Fallback
# .github/workflows/automation-scheduler.yml
name: Automation Scheduler
on:
schedule:
- cron: '0 9 * * *' # Daily at 09:00 UTC
workflow_dispatch:
jobs:
run-workflow:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run start
env:
DATABASE_PATH: /tmp/automation.db
LOG_LEVEL: info
Pitfall Guide
1. Automating Unvalidated Workflows
Mistake: Pushing automation directly into production without validating input schemas, output formats, or edge cases.
Impact: Silent data corruption, duplicate records, or cascading API failures.
Fix: Implement schema validation at ingestion boundaries. Use zod or io-ts to reject malformed payloads before execution. Log rejection reasons with correlation IDs.
2. Ignoring Idempotency
Mistake: Assuming HTTP retries or cron misfires will only trigger once. Network partitions and platform retries guarantee duplicates.
Impact: Duplicate invoices, double-charged customers, or corrupted state.
Fix: Enforce idempotency keys at the workflow level. Hash payloads, store execution records, and skip processing if the key exists. The SQLite pattern in the Core Solution demonstrates this.
3. Hardcoding Secrets or Skipping Env Validation
Mistake: Embedding API keys in code or relying on missing environment variables to fail silently.
Impact: Credential leakage, production crashes, or unauthorized API access.
Fix: Validate all configuration at startup using a strict schema. Fail fast if required variables are missing. Rotate secrets via CI/CD variables, never commit them.
4. No Structured Logging or Observability
Mistake: Using console.log or unstructured string concatenation for debugging.
Impact: Impossible to grep, parse, or alert on. MTTR spikes when failures occur.
Fix: Use pino or winston with JSON formatting. Include workflow name, execution ID, duration, and error stack. Ship logs to a centralized sink or local file with rotation.
5. Missing Exponential Backoff and Circuit Breaking
Mistake: Retrying failed requests immediately or flooding rate-limited endpoints.
Impact: IP bans, account suspensions, or cascading timeouts.
Fix: Implement exponential backoff with jitter. Add circuit breakers that pause execution after N consecutive failures. The axios interceptor pattern includes retry logic; extend it with a circuit breaker state machine for production.
6. Rate Limit Blindness
Mistake: Assuming APIs have infinite throughput. Ignoring Retry-After headers or quota limits.
Impact: Throttled requests, lost data, or degraded service for downstream consumers.
Fix: Parse rate limit headers (X-RateLimit-Remaining, Retry-After). Queue requests locally and drain at safe intervals. Use token bucket or leaky bucket algorithms for predictable throughput.
7. Skipping Local Staging Tests
Mistake: Deploying automation directly to production without mocking external dependencies.
Impact: Unexpected costs, data mutations, or service outages during testing.
Fix: Create a staging environment with mock APIs and sandbox credentials. Run workflows against deterministic fixtures. Validate idempotency, error handling, and log output before production promotion.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Low volume (<50 executions/day) | Local Node.js + SQLite | Minimal overhead, full debuggability, zero vendor fees | $0–$10/mo (VPS) |
| Medium volume (50–500 executions/day) | GitHub Actions + Cloud SQLite | Scalable scheduling, free tier sufficient, no infra management | $0/mo (GitHub free) |
| High reliability required | Code-driven engine + Sentry/Prometheus | Deterministic execution, alerting, rollback capability | $20–$50/mo (monitoring) |
| Multi-tenant SaaS automation | Event-driven + message queue (Redis/BullMQ) | Concurrency control, job prioritization, horizontal scaling | $50–$150/mo |
Configuration Template
# automation.config.yml
engine:
runtime: node:20
concurrency: 3
timeout_seconds: 30
storage:
type: sqlite
path: ./data/automation.db
retention_days: 30
observability:
level: info
format: json
output: ./logs/automation.log
rotation:
max_size_mb: 50
max_files: 5
workflows:
- name: daily-invoice-sync
schedule: "0 9 * * *"
enabled: true
retry:
max_attempts: 3
base_delay_ms: 1000
backoff_multiplier: 2
- name: webhook-ingest
type: http
port: 3000
secret_env: WEBHOOK_SECRET
enabled: true
- name: log-cleanup
schedule: "0 0 * * 0"
enabled: true
type: maintenance
Quick Start Guide
- Clone and install:
git clone <repo> && cd solopreneur-automation && npm ci
- Configure environment: Copy
.env.example to .env, set WEBHOOK_SECRET, LOG_LEVEL, and DATABASE_PATH
- Initialize database:
npm run db:init (creates SQLite file and execution_log table)
- Register workflows: Add entries to
src/registry.ts or use the YAML config parser
- Run locally:
npm run dev (starts scheduler, HTTP listener, and structured logging)
- Verify: Check
./logs/automation.log for JSON entries, trigger a test webhook, confirm idempotency on duplicate requests
The architecture is intentionally minimal. It trades visual abstraction for deterministic execution, replaces vendor lock-in with version control, and converts hidden failure states into observable, recoverable events. Solopreneurs who treat automation as infrastructure rather than convenience consistently reclaim 10+ hours weekly while maintaining sub-45-minute recovery times.