Going n.U.T.S - Developing with the nerdgeschoss Unified Tech Stack

Every nerdgeschoss project runs on the same foundation: the nerdgeschoss Unified Tech Stack. One stack, one set of conventions across every project and every client.

The philosophy fits in a sentence: make the right things easy and the wrong things hard. Stick to Rails defaults. Reach for what ships with the framework before reaching for anything else. Keep the technology boring so the features can be exciting.

A unified stack means developers can jump into any project and feel at home within hours. Codebases stay maintainable years into their lifecycle because there's nothing exotic to break. And all of the team's energy goes into solving the actual problem instead of fighting infrastructure.

If something feels too hard, you're probably doing it wrong. There's almost certainly a simpler way that works with the tools Rails gives you out of the box.

This page is the starting point for understanding how nerdgeschoss projects are built. It's written for a technical reader, whether you're a developer about to open your first project, an AI coding agent looking for architectural context, or a technically minded client evaluating our approach. For specifics on our component library and conventions, see the components documentation.

The Stack

Here's what we build with and why.

Layer Choice Why
Framework Ruby on Rails 8 The one-person framework. One developer sees a feature from database to UI.
Interaction Turbo Server-rendered pages with real-time updates. No client-side routing needed.
JavaScript Stimulus (TypeScript) Small, focused controllers for behavior, scoped to components.
Components Phlex Ruby-native view components. Testable, composable, no ERB soup.
Rich widgets React Only for highly interactive UI that needs complex client-side state. Rare by design.
Database PostgreSQL One database for everything. Persistence, job queues, caching, WebSockets.
Queues & cache Solid Queue, Solid Cache, Solid Cable The Rails 8 Solid Stack. No Redis, Postgres handles it all.
Helpers shimmer Our open-source gem. Small helpers that make Turbo and Rails a little nicer.
Testing Minitest + Oaken Rails-default test framework with fixture-based setup. Fast, parallel, no magic.
Assets Vite Asset bundling for JavaScript and CSS. Fast builds, sensible defaults.
Deployment Docker + Dokku Self-hosted on Hetzner ARM servers. No PaaS dependency.
CI/CD GitHub Actions Automated testing, review apps, and deployment on every push.

For various reasons a project might deviate from one of these choices. When that's the case, the project's README covers the details and the reasoning behind the decision.

Getting started

Every project is set up so you can go from zero to running in minutes. No installing dependencies by hand, no hunting for environment variables, no "works on my machine."

  1. Clone the repository
  2. Add the project's config/master.key file (grab it from the password manager, or pull it from the running Dokku app)
  3. Open it in VS Code
  4. Let the dev container build
  5. Hit start
  6. A browser window opens. You're looking at the app.

The master key is the only thing you need to fetch yourself. Everything else the dev container handles: Ruby version, database setup, system dependencies, seed data. If anything beyond the key requires manual setup, it's a bug and should be fixed.

Project anatomy

Open a nerdgeschoss project and you'll see... a Rails app. That's the point.

The directory structure follows Rails conventions almost entirely. Models in app/models, controllers in app/controllers, views where you expect them. No engines, no feature folders, no custom hierarchies. If you've worked with Rails before, you already know where everything is.

The one addition is app/components, which contains Phlex components. These are the building blocks of the UI, backed by the design system. If you're looking for how something renders on screen, start there.

shimmer lives in the Gemfile and adds helpers throughout the app, but it doesn't change the architecture. You won't find shimmer-specific directories or patterns. It's plumbing, not structure.

Projects with a GraphQL API have an app/graphql directory. Otherwise, standard Rails all the way down.

The project README gives a quick introduction to what the project is and documents where it deviates from the n.U.T.S. defaults. If the README doesn't mention something, assume the standard convention applies.

Architecture

A few principles run through every project. None of them are exotic. Mostly it's the Rails way, taken seriously.

Models are the center of gravity

Fat models, slim controllers. Business logic lives in models, not in controllers, not in service objects, not in "interactors" or "operations" or whatever the pattern of the month is. Models represent the domain. They're the nouns of your application, and they should behave like the real-world concepts they model.

Not every model needs to touch the database. Plain Ruby objects (POROs) are perfectly valid models. If something represents a concept in the domain, it deserves to be a class, even if it never gets persisted.

Concerns are fine, actually

As models grow, we split behavior into concerns namespaced to the model. User::Authentication, Order::Pricing, Invoice::Pdf. Each concern groups related behavior without pretending it's a separate object.

We prefer concerns over service objects. A service object scatters your domain logic across the codebase; a concern keeps it on the model it belongs to. When a plain Ruby object makes more sense, use one. But don't reach for CreateUserService when User is right there.

Domain-driven data modeling

Database schemas reflect the problem domain, not the UI. We think carefully about what entities exist, how they relate, and what the invariants are before writing any code. Rails migrations are cheap to write and expensive to get wrong, so the modeling conversation happens early.

Postgres does everything

PostgreSQL is the only database. No Redis for caching, no Elasticsearch for search, no separate message broker for async work. Solid Queue, Solid Cache, and Solid Cable all run on Postgres. One database to manage, one thing to back up, one thing that needs to be up at 3 AM.

If you're reaching for a second database, stop and ask whether Postgres can already do what you need. It almost always can.

Defaults over configuration

If Rails provides a way to do something, we use it. If the framework has an opinion, we follow it unless there's a compelling reason not to. Every custom solution is something the team has to learn, maintain, and upgrade. Every default is something Rails Core maintains for us. That math adds up fast over a decade.

The frontend

The frontend follows the same principle as everything else: let the server do the work.

Turbo drives the page

Every page is server-rendered. Navigation, form submissions, and real-time updates all go through Turbo. There is no client-side router, no frontend state management, no API layer between the Rails app and the browser. The server renders HTML, Turbo makes it feel fast.

Stimulus adds behavior

When a page needs JavaScript behavior (a dropdown, a date picker, a character counter), Stimulus controllers handle it. They're written in TypeScript and scoped to a component rather than a generic DOM element. They don't manage application state and they don't make network requests.

Phlex renders components

The view layer is built from Phlex components. Each component is a Ruby class that renders HTML, so they're testable and refactorable with standard Ruby tools.

The design team maintains a design system, and Phlex components are how that system becomes code. Reusable UI elements like buttons, cards, form fields, and modals live as shared components across the project. This keeps things visually consistent and gives developers building blocks instead of copy-paste templates. The components documentation covers conventions and patterns in detail.

React is for the hard stuff

Some interactions are genuinely complex: a drag-and-drop board, a rich text editor, a multi-step configurator with lots of client-side state. For those, we use React.

But React lives inside the Rails page. The page is still server-rendered, Turbo still handles navigation and forms. React components are islands of interactivity embedded in server-rendered HTML. They never make network requests directly. All communication with the server goes through the browser's standard form submissions and Turbo. React is a rich widget toolkit here, not an application framework.

APIs

Most nerdgeschoss applications are server-rendered with Turbo, and the "API" is just Rails controllers returning HTML. Not every project needs more than that.

When a project does need a formal API (a mobile app, a third-party integration, an external consumer, or exposing functionality to AI agents), we use GraphQL. It gives us a typed schema, introspection, and the ability to serve different clients from a single endpoint without building separate REST resources for each. The typed, self-documenting nature of GraphQL makes it especially well-suited for AI agents, which can use introspection to understand the API without additional documentation.

The split is simple: REST controllers render pages. GraphQL serves data to machines. If you're building a web feature that a human interacts with in a browser, you want a controller and a Turbo response, not a GraphQL query.

Testing

We use Minitest with Oaken for fixture-based test setup. Minitest ships with Rails, runs tests in parallel out of the box, and Oaken gives us readable fixtures without the performance overhead of factories creating database records on every run.

What we test

Model tests are the backbone. Business logic lives in models, so that's where most tests live too. If a model method can produce a wrong result, it gets a test. Keep model tests straightforward: call the method, check what changed on the model. Avoid excessive mocking. If you can just run the real code and assert the result, do that.

System tests serve as feature documentation. They follow a complete user journey, not "fill in a field and click submit" but "a user signs in, creates a project, edits it, and sees the changes reflected." They use accessibility selectors (labels, roles) instead of CSS selectors. They assert what appears on screen, not what's in the database. Read them like a description of what a human does, because that's what they are.

System tests are slow. Use them for the important flows, not for everything.

GraphQL tests cover mutations and queries when a project has an API layer.

External APIs

External services are always wrapped in a client class that acts as a facade. This client is a model like any other, and it's the only place in the codebase that knows how to talk to that service. The rest of the application calls methods on the client, not HTTP endpoints.

This makes testing straightforward. Most tests just mock methods on the client class and never touch the network. WebMock is only needed when writing tests for the client class itself, where you want to verify the actual HTTP requests. We don't use VCR. Recorded cassettes go stale and hide behavior changes.

Coverage

We don't aim for 100% branch coverage, but we do want high model coverage. Models carry the business logic, and confident deploys depend on knowing that logic is tested. Where exactly to draw the line is a judgement call, but "I didn't have time to test this" isn't one.

Deployment

Every project deploys the same way by default. Some older projects have different setups for historic reasons, which is documented in their README.

Infrastructure

Our servers are ARM machines on Hetzner, running in European data centers on renewable energy. ARM for energy efficiency, Hetzner because all data stays in Germany and GDPR becomes a non-issue rather than a legal exercise.

Dokku runs on those servers and manages applications, databases, and dependencies. Think of it as a self-hosted Heroku that we actually control. Cloudflare sits in front for SSL termination and HTTP caching.

The pipeline

  1. A developer pushes a branch and opens a pull request
  2. GitHub Actions runs the test suite
  3. A review app spins up automatically on the sandbox server, seeded with data, and posts a link as a comment on the PR
  4. The team reviews code and tests the feature on the review app. This is where QA happens.
  5. The pull request gets merged into main
  6. Main deploys to both sandbox and production automatically

The sandbox environment is a shared playground for developers and clients to try things out, not a QA gate. QA happens on review apps, where every feature gets its own isolated environment.

No release manager, no deployment schedule. Merge means deploy. The developer who merges the PR watches production afterward. You shipped it, you own it.

Backups

Databases are backed up to S3-compatible storage on Hetzner. All data stays in Germany, on European infrastructure, on green energy. That's an infrastructure decision, not a marketing bullet point.

Monitoring

Sentry handles error tracking. updown.io watches uptime from the outside. Both alert to Slack.

The expectation: if you merged the last deploy and an alert fires, that's yours. Look at it before anyone has to ask.