ai-assisted-todolist

Architecture#

The pieces#

Three deployable components and one external dependency:

Component What it is Where it lives
Frontend React 19 + TypeScript single-page app, served as static files by Apache httpd frontend/
Backend Quarkus 3.25 REST API on Java 25, also the OIDC client backend/
Database PostgreSQL 17, schema managed by Liquibase —
Identity provider Google in production; a Keycloak container in development and tests keycloak/

How a request travels#

browser ──▶ httpd ──▶ Quarkus ──▶ PostgreSQL
            (static files, and a
             reverse proxy for /api)

In production the browser talks to a single origin. httpd serves the built assets on :8080 and proxies everything under /api to the backend, so the SPA makes same-origin requests and needs no CORS handling and no API base URL. In development Vite's dev server plays the same role: it serves the app on :5173 and proxies /api to the backend on :8080.

Both proxies must preserve the original Host header — Vite with changeOrigin: false, httpd with ProxyPreserveHost On. The backend builds the OIDC redirect_uri from that header, so a rewritten host sends the browser to the wrong port after sign-in.

FallbackResource /index.html is what makes a deep link work: any path that is not a real file is a route the app resolves, not a 404. It is scoped to the document root, and ProxyPass is matched first, so an unknown /api path still returns the backend's own 404 rather than the index page.

Why the backend is a backend-for-frontend#

The backend is the OIDC client, not a resource server that validates bearer tokens the frontend obtained for itself. Quarkus OIDC runs in web-app mode: the backend performs the authorization code exchange and keeps the resulting tokens in the encrypted q_session cookie. The frontend never holds a token, and never sees one.

The consequences are worth stating, because they explain several shapes elsewhere:

authentication.md covers the flow in detail.

Backend structure#

Three packages, and no more layering than the application earns:

Panache active records mean entities expose public fields and carry their own queries. That is idiomatic Quarkus rather than an encapsulation lapse: Hibernate rewrites field access into accessor calls at build time. It is why Checkstyle's VisibilityModifier check is switched off here.

API documentation#

Every JAX-RS resource carries MicroProfile OpenAPI annotations (@Tag, @Operation, @APIResponse), and the request/response records carry @Schema examples, so the generated spec has real descriptions and sample payloads rather than bare paths. Three ways to see it:

The spec is OpenAPI 3.1, and that is load-bearing rather than incidental: only from 3.1 onwards are the component schemas JSON Schema documents, which is what lets the frontend compile them into runtime validators for every response it parses. OpenApiContractTest asserts the version, and asserts that the response records mark their fields required — a field the spec left optional would be a field the frontend accepted as missing.

Deployment#

Locally and in the end-to-end stack, httpd serves the built SPA and reverse-proxies /api, which is what makes the application same-origin. deployment.md plans the AWS shape, where that job moves to a CloudFront distribution with two origins — S3 for the SPA, the load balancer for /api — precisely so the same-origin arrangement the OIDC flow depends on survives. Nothing in it is built yet.

Schema management#

Liquibase owns the schema; Hibernate is set to validate and never generates DDL. Every schema change is its own changelog under backend/src/main/resources/db/changes/, checked in with the code that needs it, and applied at startup by quarkus.liquibase.migrate-at-start. The application therefore fails fast on a schema it does not recognise rather than quietly adapting to it.

001-baseline.xml is the whole schema as one changelog. It replaced the three that had built it up, because no database they applied to outlived them and reading three files to learn the shape of two tables was the only thing that history added.

state and importance are native PostgreSQL enum types, task_state and task_importance, not VARCHAR. The database rejects a value the application does not define rather than storing it for someone else to find. The entity says so with @JdbcTypeCode(SqlTypes.NAMED_ENUM) and a columnDefinition naming the type; without the first, the driver sends a varchar parameter that PostgreSQL will not assign to an enum column, and without the second, Hibernate looks for a type named after the Java class.

The cost is that the type and the Java enum are declared in two places. TaskEnumColumnTest is what keeps them honest: it asserts that both columns really are enum types and that their labels match the Java constants exactly, so adding a value to one and not the other fails the build rather than the first request that uses it. Adding a value later is ALTER TYPE ... ADD VALUE, which PostgreSQL allows; renaming or removing one needs a new type and a column rewrite.

Configuration#

One application.properties with profile prefixes (%prod., %dev,test., %qa.) rather than separate files per environment, so the differences between environments are visible side by side. Secrets are never checked in: production reads Google credentials from environment variables, and .env.example documents which ones. The dev and test profiles need no secrets at all, because they point at a local Keycloak with throwaway credentials.

Dev and test deliberately leave the datasource URL and the OIDC issuer unset, which is what makes Quarkus Dev Services start PostgreSQL and Keycloak automatically.

Containers and deployment#

The backend image is built by Jib rather than from a Dockerfile — ./mvnw package -Dquarkus.container-image.build=true — on an explicitly pinned eclipse-temurin:25-jre base. The pin matters: Jib's default base ships JDK 21 and cannot load this code's class files. There are no Dockerfiles under backend/ at all: the four Quarkus generated were unused, two of them JDK 17 based, and all four were deleted.

The frontend image is a two-stage Dockerfile at frontend/docker/Dockerfile: build with Node, serve with httpd.

deployment/ holds everything that deploys the app. deployment/docker/ has the Compose stacks that wire the pieces together — local-development.md describes both — and deployment/aws-tofu/ has the AWS infrastructure, written in OpenTofu; deployment.md is the plan it implements. Nothing about the application assumes AWS: it is a stateless container reading its configuration from the environment, plus a PostgreSQL it connects to by URL.