re Decisions
- Modular Monolith: Avoid microservices. A single deployable unit with strict internal module boundaries reduces network latency, simplifies data consistency, and eliminates distributed tracing complexity. Use a typed language (TypeScript/Go) to enforce boundaries at compile time.
- Infrastructure as Code: Infrastructure must be defined in code and version-controlled. This allows the entire stack to be destroyed and recreated in minutes. We recommend high-level abstractions like SST (Serverless Stack) or Pulumi over raw Terraform for solo developers, as they allow infrastructure to be written in the same language as the application.
- Containerized Deployment: Even if using serverless functions, containerization ensures local development parity. The production environment must be a binary equivalent of the local environment to eliminate "works on my machine" errors.
Technical Implementation
The core of the solution is a unified configuration that binds application code to infrastructure resources. Below is a TypeScript implementation using SST v3, which exemplifies the One-Person OS pattern by treating infrastructure as a function of your application state.
Project Structure:
project-root/
βββ infra/
β βββ main.ts # Infrastructure definition
βββ src/
β βββ api/ # API handlers
β βββ workers/ # Background jobs
β βββ shared/ # Shared types/utils
βββ sst.config.ts # SST configuration
βββ package.json
Infrastructure Definition (infra/main.ts):
This file defines the entire stack: API, Database, Cron jobs, and Storage, with automatic environment isolation.
import { api, db, bucket, cron } from "./resources";
export const stack = sst.stack(function (ctx) {
// 1. Database: Managed Postgres with automatic backups
const database = new db.Postgres(ctx, "db", {
version: "15",
// Auto-pause for cost efficiency during low traffic
scale: "free",
});
// 2. Storage: S3-compatible bucket with lifecycle policies
const assets = new bucket.Bucket(ctx, "assets", {
transform: {
bucket: (args) => {
args.lifecycleRules = [{
id: "cleanup-temp",
enabled: true,
expiration: { days: 30 },
prefix: "tmp/",
}];
},
},
});
// 3. API: TypeScript API with edge optimization
const api = new api.Api(ctx, "api", {
routes: {
"GET /health": "src/api/health.handler",
"POST /webhooks": "src/api/webhooks.handler",
},
defaults: {
function: {
environment: {
DB_URL: database.url,
ASSET_BUCKET: assets.name,
},
// Automatic IAM permissions
permissions: [assets],
},
},
});
// 4. Cron: Scheduled tasks for maintenance
new cron.Cron(ctx, "cleanup-cron", {
schedule: "rate(1 day)",
job: "src/workers/cleanup.handler",
environment: {
DB_URL: database.url,
},
});
// 5. Outputs for CI/CD integration
ctx.addOutputs({
ApiUrl: api.url,
DatabaseUrl: database.url,
});
});
Rationale:
- Permissions: The
permissions: [assets] line automatically generates IAM policies. No manual policy JSON.
- Environment: Secrets and config are injected automatically via the framework, eliminating manual secret management.
- Scale: The
scale: "free" option on the database allows the app to scale to zero cost during inactivity, crucial for side projects, while handling burst traffic via serverless compute.
CI/CD Pipeline (github/workflows/deploy.yml):
A single workflow handles testing, infrastructure updates, and deployment.
name: One-Person OS Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
# Run tests
- run: npm test
# Deploy Infrastructure and Code
- run: npx sst deploy --stage production
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
This pipeline is idempotent. Running it repeatedly yields the same result. It handles database migrations, infrastructure changes, and code updates in a single atomic operation.
Pitfall Guide
Solo developers face unique risks due to the lack of peer review and redundancy. The following pitfalls are critical to avoid.
-
The Backup Delusion
- Mistake: Assuming the cloud provider handles backups automatically or relying on manual snapshots.
- Reality: Managed databases often retain backups for only 7 days. Ransomware or accidental deletion can be permanent.
- Best Practice: Implement cross-region backup replication. Write a script that periodically dumps the database to a separate storage bucket with versioning enabled. Test restores quarterly.
-
Microservices Premature Optimization
- Mistake: Splitting services to "learn Kubernetes" or "be scalable."
- Reality: This introduces distributed transactions, network latency, and complex deployment graphs. A solo developer cannot effectively monitor five services.
- Best Practice: Stick to a Modular Monolith. Split only when a specific module has independent scaling requirements that cannot be met by the monolith's architecture.
-
Secret Sprawl
- Mistake: Storing API keys in
.env files locally and copying them manually to production, or worse, hardcoding them.
- Reality: Manual secret rotation is error-prone. Leaked keys in a solo project can lead to bill shock or data breaches.
- Best Practice: Use a secrets manager integrated with your IaC. Tools like AWS Secrets Manager or Cloudflare Secrets, injected via your deployment tool, ensure secrets never touch your codebase or local disk.
-
Observability Blind Spots
- Mistake: Relying on
console.log or checking the server only when users report issues.
- Reality: By the time a user reports an error, the context is lost. Silent failures accumulate.
- Best Practice: Implement centralized error tracking (e.g., Sentry) and uptime monitoring from day one. Configure alerts to trigger on error rate spikes, not just total failures.
-
Vendor Lock-in Paralysis
- Mistake: Avoiding managed services due to fear of lock-in, leading to reinventing wheels.
- Reality: For a one-person team, the cost of building and maintaining a custom solution far exceeds the migration cost of switching vendors later.
- Best Practice: Abstract critical interfaces. If using a database, use an ORM or query builder that supports multiple dialects. If using auth, wrap the provider SDK. This allows migration without rewriting business logic.
-
Manual Deployment Steps
- Mistake: "I'll just SSH in and run the script."
- Reality: Manual steps are not repeatable. They break under stress and cannot be automated.
- Best Practice: Every operation must be scriptable. If you do it twice, automate it. The deployment process should be a single command triggered by a git push.
-
Ignoring Security Headers
- Mistake: Focusing on functionality and neglecting HTTP security headers, CORS policies, and rate limiting.
- Reality: Solo apps are frequent targets for automated bots scanning for vulnerabilities.
- Best Practice: Use middleware that enforces security headers by default. Implement rate limiting on all public endpoints. Regularly run dependency audits (
npm audit).
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Hobby / Prototype | Vercel + Supabase + GitHub Actions | Zero infra overhead. Focus purely on code. | Low ($0-$20/mo). High lock-in risk. |
| Revenue-Generating Startup | One-Person OS (SST/Pulumi + AWS) | Full control, scalable, audit-ready. Balances cost and ops. | Medium ($50-$100/mo). Low lock-in via abstraction. |
| High-Compliance (FinTech/Health) | Modular Monolith + Dedicated VPC + IaC | Strict security boundaries, data residency control. | High ($100-$300/mo). Requires deeper expertise. |
| AI/ML Workload | Modular Monolith + GPU Spot Instances | Cost optimization for compute-heavy tasks. | Variable. Depends on utilization. |
Configuration Template
Copy this sst.config.ts as a baseline for your One-Person OS. It includes production-grade defaults for security, scaling, and observability.
// sst.config.ts
import { SSTConfig } from "sst";
import { Stack } from "./infra/main";
export default {
config(_input) {
return {
name: "my-one-person-os",
region: "us-east-1",
// Enable automatic SSL and security headers
transform: {
api: {
gatewayHttp: (args) => {
args.cors = {
allowOrigins: ["https://myapp.com"],
allowMethods: ["GET", "POST", "PUT", "DELETE"],
allowHeaders: ["Content-Type", "Authorization"],
};
},
},
},
};
},
stacks(app) {
// Load stack only in specific stages to prevent accidental prod changes
if (app.stage !== "dev") {
app.stack(Stack);
}
},
} satisfies SSTConfig;
Quick Start Guide
Get your One-Person OS running in under 5 minutes.
-
Initialize Project:
npx create-sst@latest my-os --template app
cd my-os
npm install
-
Define Infrastructure:
Replace infra/main.ts with the architecture definition from the Core Solution. Add your application modules.
-
Configure Environment:
Create .env.local with your AWS credentials or configure your cloud provider CLI.
npx sst dev
This starts the local development environment with hot-reloading and local resource mocking.
-
Deploy to Production:
npx sst deploy --stage production
This provisions all cloud resources, runs migrations, and deploys code. The CLI outputs the API endpoint and database connection string.
-
Verify:
Run the provided health check endpoint. Confirm logs appear in your cloud provider's logging service. Your One-Person OS is now live and automated.