24 min read

My RAD Stack

Table of Contents
ℹ️

This stack reflects my own constraints and preferences, not a universal prescription. If your context, scale, or existing toolset differs, your choices should probably differ too. Treat this as input to your own design process β€” not a blueprint to copy. You are responsible for evaluating whether any of this is appropriate for your situation.

Why This Stack?

The term Rapid Application Development (RAD) carries baggage β€” in the nineties it described visual builders and code generation from forms. I am using it literally: building applications rapidly, without ceremony. The target is a specific class of problem β€” prototypes, internal tools, exploratory builds, and focused production services where operational overhead is the enemy of progress. For all of these, the answer is one binary, one database file, one host to manage.

That answer kept repeating across projects, but handled ad-hoc each time, the result was inconsistency β€” good decisions remade from scratch, shortcuts that looked temporary but became permanent, a foundation rebuilt rather than reused. This stack is the decision to stop doing that: make the starting-point choices once, deliberately, and build a template the next project can start from without ceremony.

Some of these components have been used on real projects β€” those I have direct confidence in. Others I have used individually but not yet in this exact combination. A few are here because the design rationale is sound, but only use will confirm they hold up as a whole. This is a formalized hypothesis, not a proven recipe. The template exists. The validation is ongoing.

The Philosophy

These goals are the lens through which every technology choice is evaluated. When trade-offs arise, they are resolved by asking which option best serves these outcomes.

Goal What it means
Function over form The application works correctly and reliably. Aesthetic refinement is deferred β€” a working tool with a plain interface beats a polished prototype that does not quite function.
Fast and pleasant to develop A fast rebuild loop, clear structure, and no tooling friction. The codebase has a low framework-specific mental model β€” the patterns are standard and explicit, with minimal magic to decode.
Simple to deploy, deployable everywhere Deploying the application is a single, repeatable step β€” no environment-specific build process, no container registry, no orchestration layer. The same artifact runs anywhere.
Simple to operate Fewer moving parts means fewer things to break, patch, or upgrade. Low runtime overhead, a small monitoring surface, and standard tooling for observability β€” no specialist knowledge required.
Easy to evolve When requirements change β€” and they will β€” the stack should not resist. Individual parts can be swapped for more mature alternatives when the need arises, or broken out into separate services when the application outgrows the monolith. No framework lock-in, no tight coupling between layers that makes either move expensive.

The Principles

The principles translate the philosophy into concrete engineering decisions. They are technology-agnostic β€” the same principle can be satisfied by different implementations. The Technology Choices section maps each principle to specific tools.

Application

Principle Rationale
Flexible output modes The application can run as a standalone background service with no user interface, a web API, a browser-rendered web application, or any combination. The same stack supports all three without switching technologies or introducing separate runtimes.
The browser is the GUI platform All user-facing interfaces are delivered through the browser β€” no native clients, no thick clients, no separate runtime to install. Internal tools use the browser for the same reason external products do: it is universal, requires no deployment to end users, and works everywhere.

Frontend Architecture

Principle Rationale
Server-first architecture (SSR) Logic, data, and rendering live on the server. The server sends complete HTML β€” the browser renders it, not assembles it. No client-side state management, no API contract between frontend and backend, no hydration step.
Progressive enhancement Dynamic interactions update specific parts of the page, not the whole page. The server responds with targeted HTML fragments that replace sections of the DOM β€” no full-page reloads, no client-side construction from JSON.
Minimal client-side JavaScript Client-side JavaScript is added when genuine client-side interactivity is needed and sprinkled on top of working HTML β€” not used as the foundation. The baseline works without it; JavaScript enhances it.
Semantic, class-light styling HTML elements are styled directly through a semantic CSS baseline β€” no utility-class proliferation, no CSS-in-JS, no build-time stylesheet generation. Markup stays clean and readable; CSS follows the content structure rather than decorating it with class names.

Deployment & Infrastructure

Principle Rationale
Common denominator infrastructure The target deployment environment is a VM or bare metal with an OS. No container runtime, no managed services, no cloud-provider-specific primitives β€” anything that runs Linux can run this application.
Lean on the platform Where the OS or platform already provides a capability β€” process management, scheduling, logging β€” the stack uses it rather than replacing it. Every additional service must earn its place; nothing is added speculatively.
Protected ingress The application is never exposed directly to the internet. All public traffic enters through a protection layer β€” a Cloudflare Tunnel or equivalent. No inbound ports on the host, no public IP required, no direct attack surface.
Monorepo Application code, configuration, scripts, and documentation all live in a single repository β€” one place to find everything, one change to ship everything. No cross-repo coordination overhead.
Infrastructure and Configuration as Code Infrastructure provisioning and service configuration are defined as code β€” version-controlled, reviewable, and repeatable. No manual steps, no undocumented configuration drift. The repository is the source of truth for both what runs and how it is configured.
One deployable application Application code, templates, and static assets are bundled into a single deployable unit β€” no separate asset pipeline or content server required.
Embedded persistence Application data lives in an embedded database on the same host. No separate database process, no network round-trip, no connection pool β€” persistence is part of the application, not a separate service to manage.
Production-ready from day one The development environment mirrors production. There are no β€œit works locally” surprises because local and production run the same application.
Deploy from local machine The application can be deployed directly from a developer machine. A CI/CD pipeline can be added when it earns its place β€” it is not a prerequisite for shipping.
Multi-tenancy by deployment Multi-tenant isolation is achieved by deploying separate instances, not by building tenant partitioning into the application. Each tenant gets a dedicated binary, database, and host β€” complete isolation with no shared-state coordination. The stack’s low operational cost makes per-tenant deployment practical.
Scale up before out A single well-resourced host handles more load than most applications built with this stack will ever see. Vertical scaling β€” more CPU, RAM, and faster storage β€” is the first response to capacity pressure. If a single well-resourced host is no longer enough, the application has probably outgrown the scope of this stack β€” and the right response is rearchitecture, not forcing horizontal scaling onto a foundation that was never designed for it.
Recovery over redundancy The target is recovery time, not zero-downtime failover. The stack is designed to be fully recoverable from any failure mode without manual guesswork β€” through VM snapshots, continuous data replication, and automated infrastructure provisioning.

Quality

Principle Rationale
Fail at build time, not runtime Where possible, errors are caught at compile time rather than discovered in production. Type safety across the stack β€” templates, data access, API boundaries β€” ensures whole classes of bugs surface during development, not after deployment.
Generate from source of truth Where a higher-level artifact defines the contract β€” a template, a schema, an API specification β€” implementation code is generated from it rather than written by hand. The generated code is always in sync with its source, and inconsistencies surface at build time rather than at runtime.
Test the interfaces, not the internals Automated tests cover the critical paths β€” the API surface and GUI behavior that must not break. Tests target the observable boundaries of the application (APIs, GUI interactions, file outputs), not internal implementation details. This keeps the test suite stable as the codebase evolves, and avoids testing for coverage metrics rather than confidence.
Manual exploration Manual exploration catches what tests do not anticipate. Running and using the application directly surfaces edge cases, usability issues, and unexpected interactions.

The Technology Choices

Every component earns its place. The tables below map the full stack across twelve groups β€” from the application code down to the physical infrastructure β€” and explain what each component contributes and why it is there. Not every group applies to every application, and not every function within a group does either β€” this is a reference set to draw from, not a mandatory checklist. A background service needs none of the frontend rows; an API-only application skips the templating layer; a simple internal tool may never need session management or analytical storage. Each application takes what it needs.

Application (server)

Function Technology Purpose
Programming Language Go programming language Statically compiled with no external runtime β€” the binary is the complete application. Go specifically: fast compile times keep the rebuild loop tight; goroutines and channels handle concurrency and long-running background services natively; and the standard library covers HTTP, JSON, cryptography, and structured logging without external packages. Deployment is copying a file β€” no runtime to install, no version to manage.
Router Chi Routing and middleware. Lightweight, idiomatic, and explicit β€” no magic, no framework conventions to decode.
Input Validation go-playground/validator Struct-level validation for incoming request data. Validates fields by tag β€” no custom validation boilerplate for common constraints.
Password Hashing golang.org/x/crypto Bcrypt hashing for credential storage and verification. Passwords are stored as bcrypt hashes and verified at login against the submitted value β€” no additional runtime, no external service.
Session Management alexedwards/scs Server-side session management backed by SQLite β€” no separate session store required. Stores the authenticated user identity and role in the session after login. Chi middleware reads the session to enforce role-based access per route group.
Static Assets Go embed HTML, CSS, JS, images, and fonts bundled into the application binary β€” no separate asset pipeline or content server required.
Data Access sqlc Type-safe SQL queries generated from annotated .sql files. SQL is validated at generation time β€” query errors surface before deployment, not at runtime.
Database Driver modernc.org/sqlite Pure Go SQLite driver β€” no CGO required. Keeps the binary self-contained and the build process simple.
Schema Migrations goose Database schema migration tool. Migrations are plain SQL or Go functions, version-controlled alongside the application code. Applied at startup or via a Task target.
Structured Logging slog Structured, levelled log output written to stdout. Captured and retained by the OS journal β€” no separate log aggregation required. A request ID is attached to every log entry via middleware, making cross-entry correlation straightforward with standard tools like journalctl and jq.
Health Check /healthz Lightweight endpoint that returns the application status. No dependencies, no auth β€” designed to be called from a Task target for quick operational checks from the developer machine.

Frontend Architecture (browser-delivered)

Function Technology Purpose
HTML Templating templ Type-safe server-side HTML templates compiled at build time. Template errors surface before deployment, not at runtime.
Progressive Enhancement htmx Server-driven partial page updates. The server returns HTML fragments β€” no JSON, no client-side state management, no hydration step.
Client-side Interactivity Alpine.js Lightweight interactivity layer for behaviors that belong in the browser β€” dropdowns, modals, tab panels, conditional visibility, and reactive form interactions. Handles local state and event-driven behavior without a server round-trip and without pulling in a full JavaScript framework.
Styling Framework Pico CSS Semantic CSS baseline that styles HTML elements directly β€” no utility-class proliferation, no CSS-in-JS, no build step. Clean markup with sensible defaults that cover the majority of styling needs without custom overrides.

Storage (server)

Function Technology Purpose
Transactional Database SQLite (WAL Mode) Embedded relational database for operational data. No separate process, no network round-trip, no connection pool β€” the database is a file on disk.
Database Backup Litestream Continuous SQLite replication to any S3-compatible store or SFTP target with no impact on reads or writes. Provides point-in-time restore with near-zero operational cost.
Analytical Database DuckDB Analytical database for OLAP workloads. Accessed via the DuckDB CLI or Python analytics scripts β€” not embedded in the Go application binary, which avoids the CGO dependency the Go DuckDB bindings require. Queries large datasets without a separate server or ETL pipeline.
Analytical Files Parquet Columnar file format for analytical data at rest. Written by the Go application or exported from DuckDB β€” efficient, compressed, and readable by any analytics tooling without a database layer.
Plain Files OS filesystem For data that does not need a relational model β€” configuration, logs, exported reports, or structured content that fits naturally in files. Read and written directly without a database layer.
File Backup restic Encrypted, deduplicated backup of files and directories to any S3-compatible store or SFTP target. Supports incremental backups with point-in-time restore β€” no backup server required.
Object Storage Any S3-compatible store or SFTP Off-host destination for Litestream database replicas and restic file backups. Both tools support S3-compatible stores (Cloudflare R2, Backblaze B2, AWS S3, or self-hosted MinIO) and SFTP. SFTP makes a NAS device with SSH access a valid backup target without any additional software β€” no S3 compatibility required.

Application Scripts (developer machine and server)

Function Technology Purpose
Analytics Processing Polars Python library for high-performance DataFrame operations on Parquet files. Used in analytics scripts for batch processing, exports, and reporting β€” not embedded in the Go application.

Build (developer machine)

Function Technology Purpose
Compilation go build Compiles the application into a single statically linked binary. No linker scripts, no build system beyond the Go toolchain β€” one command produces the deployable artifact.
Template Generation templ generate Generates Go code from templ templates. Runs as part of the build and during development.
Query Generation sqlc generate Generates type-safe Go functions and structs from annotated .sql files. Runs as part of the build and during development.
API Specification OpenAPI Spec When the application exposes an API, the contract is defined first as an OpenAPI specification. Server stubs and types are generated from the spec β€” the implementation conforms to the contract, not the other way around.
API Code Generation oapi-codegen Generates Go server interfaces and types from the OpenAPI spec. Runs as part of the build alongside templ generate and sqlc generate.
Linting golangci-lint Runs go vet, gofmt, errcheck, staticcheck, gosec, and more through a single invocation with consistent output and a single configuration file. No separate tool invocations, no differing output formats β€” one command covers correctness, style, error handling, and security.
Vulnerability Scan govulncheck Official Go vulnerability checker. Scans dependencies against the Go vulnerability database β€” surfaces known CVEs before deployment, not after.
Secret Scanning Gitleaks Scans the git repository for accidentally committed secrets β€” API keys, tokens, passwords, and private keys. Runs as a pre-commit hook and as a Task target. Catches credential leaks before they reach the remote.

Testing (developer machine)

Function Technology Purpose
Unit Testing go test Standard Go test runner. Tests are written as plain Go functions in _test.go files alongside the code β€” no separate test framework required. Run via Task as part of the standard build pipeline.
API Testing Bruno API collections stored as plain .bru files alongside the code β€” version-controlled and diffable. Supports both manual exploration and automated runs from Task, bridging interactive testing and scripted verification.
GUI Testing Playwright Automated browser tests for GUI interactions. Tests routes, page content, and interactive behavior as a user would.

DevOps Platform (developer machine)

Function Technology Purpose
Version Control Any git server Source control. Self-hosted or managed β€” the stack has no dependency on a specific provider.
Deployment Automation Ansible Repeatable, idempotent deployment from a local machine. Copies the binary, manages systemd service files, and configures the host β€” no CI/CD pipeline required.
Secrets Management Ansible Vault Encrypts sensitive values β€” credentials, API keys, tokens β€” in a version-controlled YAML file. The vault password lives in a password manager, not the repository. Ansible decrypts at deploy time and writes secrets to a root-owned systemd EnvironmentFile on the host, injecting them as environment variables into the application process.
Task Runner Task Orchestrates common development and deployment tasks β€” building, testing, generating, and deploying β€” through a single, discoverable entry point. Tasks are defined in a Taskfile.yml alongside the code β€” version-controlled, readable, and self-documenting via task --list.
Development Reload Air Watches for file changes and rebuilds the binary. No bundler, no hot module replacement, no Node.js process.
Dev Environment mise Pins exact versions of every development tool β€” Go toolchain, templ, sqlc, golangci-lint, Air, Python, and others β€” in a version-controlled .mise.toml. Any developer running mise install gets the same tool versions without Docker, containers, or a Nix dependency. Also manages Python virtual environments, auto-creating and activating a .venv when entering the project directory. Coexists with system package managers β€” project versions shadow global installs within the project directory only.

Platform Scripts (developer machine and server)

Function Technology Purpose
Automation Scripts Bash / Python Automation scripts for tasks the Taskfile orchestrates. Bash for simple shell tasks; Python when more logic or structure is needed.
Health Check task health A Task target that curls the /healthz endpoint and reports the application status. Quick operational check runnable from the developer machine without opening a browser.

Documentation (server β€” authored locally, served by the application)

Function Technology Purpose
Prose Documentation Markdown + goldmark Documentation written in Markdown and rendered to HTML by the application. Served at a /docs endpoint β€” no separate documentation site or deployment required.
API Documentation OpenAPI + Redoc API documentation generated from the OpenAPI spec and served at /api/docs. Stays in sync with the spec that drives code generation β€” no separate API docs deployment.
Diagrams PlantUML / Mermaid Architecture, sequence, and flow diagrams defined in markup β€” version-controlled and diffable alongside the code. Generated as SVG files in the build pipeline and embedded in the /docs endpoint.

Application Platform (server)

Function Technology Purpose
Operating System Rocky Linux Hardened OS baseline with SELinux enforcing, regular security errata, and RHEL compatibility β€” security is part of the platform, not bolted on.
Mandatory Access Control SELinux Confines processes to the minimum access they require β€” a misconfigured service cannot escape its defined permissions, regardless of other controls.
Host Firewall firewalld Restricts network access to the host, permitting only what is explicitly required β€” SSH and any configured ingress β€” and blocking everything else. Reduces the attack surface at the host boundary regardless of the ingress layer in use.
Process Management systemd Manages all services on the host as independent units β€” the application, reverse proxy, and any supporting services. Service units define restart-on-failure behavior, resource limits, and filesystem sandboxing β€” application hardening configured as code.
Scheduled Tasks systemd timers Periodic and time-based task execution managed by systemd. No separate cron daemon β€” timer units are version-controlled, observable, and managed alongside the application services.
Remote Access SSH Secure shell access for administration and Ansible connectivity. Key-based authentication only β€” password authentication and root login are disabled. A dedicated deploy user with limited sudo permissions is the only account Ansible uses.
OS Updates dnf-automatic Automatic security patch application for all dnf-managed packages β€” OS, Caddy, and cloudflared all provide official RPM repositories and are updated automatically. Binary installs not managed by dnf (Litestream, restic) are version-pinned in Ansible and updated on each deployment run.
DNS Cloudflare DNS / self-hosted Maps the domain name to the Cloudflare Tunnel endpoint. Internal or air-gapped deployments use a self-hosted DNS resolver β€” any DNS server that resolves the application hostname on the local network. No dependency on a specific DNS provider.
Public Ingress Cloudflare Tunnel The sole entry point for public internet traffic. Connects the host to Cloudflare’s network without opening any inbound ports β€” DDoS protection, access control, and TLS termination without exposing a public IP address.
Reverse Proxy Caddy Reverse proxy between the ingress layer and the application. Handles routing, response compression, and security header injection. Provides automatic HTTPS β€” certificates are managed without manual configuration, whether traffic arrives from a Cloudflare Tunnel, a local network, or directly from the internet.

Virtual Infrastructure (server)

Function Technology Purpose
Bare Metal Direct OS install Rocky Linux installed directly on the hardware β€” no hypervisor layer. The simplest self-hosted option when VM isolation and snapshots are not required.
Hypervisor Proxmox Self-hosted virtualization on own hardware. Manages Rocky Linux VMs with isolation, snapshots, and resource allocation. The choice when deploying locally rather than through a cloud provider.
Cloud VM Any cloud provider When not self-hosting, any cloud provider’s VM service takes the place of Proxmox. The stack has no cloud provider dependency β€” the VM only needs to run Rocky Linux.
VM Snapshots Proxmox Backup / Cloud Provider Scheduled VM snapshots provide a full-system recovery point at the hypervisor level β€” OS, configuration, application binary, and database in a single restore operation. Proxmox includes built-in VM backup scheduling; cloud providers offer equivalent snapshot services as standard. For bare metal without a hypervisor there is no snapshot layer β€” recovery falls back to Ansible provisioning and data restore from Litestream and restic.

Physical Infrastructure (server)

Function Technology Purpose
Compute Bare metal / Raspberry Pi The physical host when not using a cloud provider. Runs Rocky Linux directly or Proxmox as the hypervisor β€” any hardware they both support. A repurposed server, a NUC, or a Raspberry Pi all qualify.

Roads Not Taken

The section title is a deliberate β€” if not particularly clever β€” nod to Robert Frost. Several roads diverged. The ones below are real paths β€” well-worn, with good reasons to take them. This stack took a different one, and these are the forks worth explaining.

Alternative Why not
Containers (Docker Compose, Kubernetes) The application is a single statically compiled binary β€” Docker Compose adds a runtime, image management, and volume overhead for problems this stack does not have. systemd manages the process with lower overhead and a tighter security model. Kubernetes scales these concerns up, not down.
Fullstack frameworks (SvelteKit, Next.js) Designed for stateless, short-lived request handlers β€” not long-running background services or direct OS integration. Go handles the full range: one binary, managed as a daemon, with goroutines and signal handling a Node-based framework is not built for. templ and htmx cover the frontend without pulling a bundler or a second runtime into the deployment.
TypeScript everywhere (Hono, Elysia) Go produces a single statically linked binary with no runtime and no node_modules on the server. Go’s concurrency model β€” goroutines and channels β€” handles parallel workloads in ways that Node, Bun, and Deno’s event loop model is not designed for.
ORM (GORM, ent) sqlc generates type-safe Go functions directly from annotated SQL β€” readable, reviewable, and validated at build time. ORMs abstract this behind a query builder, adding an API surface to learn and debug around without a corresponding gain for this stack’s data access patterns.
PostgreSQL SQLite in WAL mode handles this stack’s read/write patterns on a single host with no separate process, no connection pool, and no network round-trip. When the application outgrows it, the migration path to PostgreSQL is well-understood β€” the stack is designed not to resist it.
CI/CD pipeline The stack deploys directly from a developer machine via Ansible β€” one command, repeatable, idempotent, and auditable. For a solo developer or small team, pipeline overhead adds coordination cost before it adds value. The stack is not designed to resist it when the time comes.
PaaS (Railway, Fly.io, Render) A bare VM has a fundamentally wider operational envelope β€” full control over the process, filesystem, and network. PaaS requires containers, constrains what the application can do, and creates a vendor dependency that the β€œdeployable everywhere” principle explicitly avoids.

The Bottom Line

The RAD Stack is not a universal answer β€” it is a specific set of trade-offs made for a specific category of problem. It gives up horizontal scalability for operational simplicity. It gives up client-side flexibility for server-side predictability. It gives up ecosystem breadth for deployment clarity. For prototypes, internal tools, and focused production services β€” where getting to working, maintainable software quickly matters more than planning for scale that may never arrive β€” those are not losses. They are the design.

Most of the technologies in this stack have track records in production individually β€” those carry direct confidence. A few are here on the strength of design rationale rather than personal use. What remains a hypothesis in either case is this specific combination β€” that they hold together as coherently as they hold apart. Building with the stack is how that gets tested.


πŸ“ž

Faced the same constraints β€” rapid iteration, minimal overhead, production-ready without a platform team β€” but arrived at different principles or a different stack? The disagreements are the interesting part. I’m interested in the conversation.

πŸ“‘

The hypothesis gets tested as I build with it. I’ll document what holds up and what needs revisiting. Follow my RSS feed to catch what comes next.