Case study · Open source

Bridopen

A server-rendered notes application written in Go, built as a layered modular monolith with no web framework and no SPA. Published under the MIT licence so the architecture, the security decisions and the test suite can be read and challenged.

Role
Sole author
Stack
Go 1.27 · PostgreSQL
Licence
MIT
Source
Public on GitHub

Overview

Quick notes tend to end up in scratch files, chat threads and an untrusted note app, with no account protection and nothing that can be audited. Bridopen is a single, self-hosted place to write, tag, attach files to and archive notes, aimed at individuals and small teams that would rather run their own instance than hand the content to a third party. It is a personal engineering project, published open source so the decisions behind it are inspectable rather than asserted.

My contribution

I am the sole author of the repository. I designed the layering, wrote the HTTP handlers, services, domain models and repositories, the SQL migrations, the HTML templates and the browser JavaScript, the security middleware, and the test suite. I also wrote the Docker Compose setups, the Makefile targets and the documentation.

The project exists to practise end-to-end engineering decisions — boundaries between layers, session and authorization handling, upload safety, migrations and release versioning — in a codebase I control completely and can therefore publish in full.

Architecture

A modular monolith in layers. cmd/http is the composition root: it loads configuration, opens the database pool, wires the session manager, mailer, storage, scanner, repositories, services and handlers, then registers the routes. Business modules live under internal/handlers (notes, users, legal); cross-cutting infrastructure lives under internal/platform.

Request flow through Bridopen A browser request passes through the middleware chain — security headers, IP rate limiting, CSRF protection, the session manager and the authentication middleware — then reaches a handler. The handler calls a service, which calls a repository for PostgreSQL, the attachment storage on disk (optionally scanned by ClamAV), or the SMTP mailer. The handler renders an html/template response back to the browser. Browser HTML form / GET MIDDLEWARE CHAIN Security headers CSP, X-Frame-Options IP rate limit sliding window per IP CSRF gorilla/csrf Session scs + pgxstore Auth + access log requires signed-in user Error handling Handler HTTP, DTO, render Service use cases, validation Repository SQL, scans, transactions SMTP mailer confirmation, recovery Attachment storage local volume, outside the DB optional ClamAV scan PostgreSQL notes, users, sessions, audit Dashed: rendered html/template response returned to the browser
Request flow, drawn from cmd/http/routes.go, cmd/http/security.go and internal/platform/middleware.

Layer rules

  • Handlers deal with HTTP only: session, request parsing, response, rendering and JSON.
  • Services hold use cases, flow validation and orchestration.
  • Models hold entities, states, constants and pure domain rules.
  • Repositories isolate SQL, row scanning, transactions and persistence mapping.
  • Templates receive DTOs and page models, never business rules.

Stack

  • Backend: Go 1.27 with the standard net/http router; no web framework.
  • Data: PostgreSQL via pgx/v5; schema managed by golang-migrate.
  • Sessions: alexedwards/scs with the PostgreSQL store.
  • Views: html/template rendered server-side, Tailwind CSS and plain JavaScript split by responsibility.
  • Delivery: Docker Compose for development and production, Caddy in front, Git tags as the version of record.

Engineering decisions

Three decisions that shaped the codebase, each with the alternative considered and what it costs.

1. Standard library HTTP instead of a web framework

Decision:
routing, middleware and request handling are built on net/http alone, using the method-and-path patterns available in the standard ServeMux.
Alternative:
Gin, Echo or Chi, which would have supplied routing groups, binding and a middleware stack out of the box.
Reason:
a small dependency surface, no framework idioms leaking into handlers, and layer boundaries that have to be stated explicitly rather than inherited.
Limitation:
the middleware chain, the error mapping and the request helpers are hand-written, so there is more of my own code to maintain and test than a framework would require.

2. Server-side rendering instead of an SPA

Decision:
pages are rendered with html/template and progressively enhanced with plain JavaScript; there is no client framework and no bundler.
Alternative:
a JSON API with a React or Vue front end, deployed separately.
Reason:
one deployable artefact, state kept on the server with the session, and no build pipeline to keep in sync with the backend.
Limitation:
richer client interactions have to be written by hand. Tailwind is currently loaded from a CDN at runtime, which forces script-src 'unsafe-inline' in the Content-Security-Policy; compiling the stylesheet at build time and tightening that directive is an open item.

3. Database-backed sessions instead of stateless tokens

Decision:
sessions are stored in PostgreSQL through scs with the pgx store, and a separate table records the user's active sessions.
Alternative:
a signed, stateless token (JWT) in a cookie, validated without touching the database.
Reason:
a signed-in user can list their active sessions and revoke them from the account page, and signing out actually invalidates the session server-side rather than waiting for a token to expire.
Limitation:
every authenticated request performs a session lookup against the database, so the database is on the critical path for all authenticated traffic.

Quality and security

Everything below is present in the public repository and can be checked against the source. Nothing here is a claim about how secure a deployed instance is — that depends on how it is configured and operated.

Tests and CI

  • 26 test files across handlers, services, repositories, models, DTOs and the platform packages.
  • Written with testify; repository tests use pgxmock/v4 so SQL and scanning are exercised without a live database.
  • GitHub Actions runs go vet ./... and go test ./... on every push to main and every pull request.
  • Reproduce locally with go test -count=1 ./....

Access controls

  • Passwords hashed with bcrypt.
  • TOTP two-factor authentication (6 digits, 30-second period, one-period validation window) with QR enrolment and recovery codes; the shared secret is encrypted at rest.
  • CSRF tokens via gorilla/csrf, secure cookie flags, and a captcha on the authentication forms.
  • Per-IP rate limiting and security headers — Content-Security-Policy, X-Frame-Options: DENY, X-Content-Type-Options, Referrer-Policy, Permissions-Policy.

Data and operations

  • Seven versioned SQL migrations under db/migrations, applied with golang-migrate.
  • Business events recorded in a dedicated audit schema.
  • Attachments written to a volume outside the database; when a ClamAV address is configured, uploads are scanned before being persisted.
  • Account export and account deletion available to the signed-in user from the profile page.
  • Releases tagged vMAJOR.MINOR.PATCH; the running build reports version, commit and build time at GET /version.

Results and limitations

What exists today is a working application: sign-up with email confirmation, sign-in with optional two-factor authentication, password recovery, notes with tags, attachments, archive and trash with reversible deletion, light and dark themes, account export and deletion, and public legal pages. It runs from a single docker compose setup with migrations applied by a Make target.

Stated limitations

  • No performance benchmark has been run. I have no measured latency, throughput or uptime figures for this project, so none are claimed.
  • No official hosted instance. There is no public demo to link to. Anyone who deploys Bridopen is responsible for the service, its users' data and the legal texts on the public pages.
  • Attachment scanning is opt-in. When BRIDOPEN_CLAMAV_ADDR is not configured, the application falls back to a no-op scanner and uploads are stored without being scanned.
  • The legal pages are generic templates. They need legal review before any real deployment.
  • The Content-Security-Policy is not as strict as it could be. Loading Tailwind from a CDN at runtime requires allowing inline scripts.
  • There is no real-world usage data. This is a personal project; I am not claiming users, adoption or production traffic.

Explore

The repository is public and includes the README with setup instructions, the environment variable reference, the Make targets and the supporting documentation under docs/.

The repository README is written in Portuguese. There is no hosted demo instance, by design.