A modern full-stack AI-powered chat application built with FastAPI, Next.js, PostgreSQL, and Docker.
The project consists of a Python backend exposing REST APIs and AI integrations, and a React frontend built with Next.js. The application is fully containerized using Docker Compose, making it easy to develop, test, and deploy consistently across environments.
- FastAPI backend
- Next.js frontend
- PostgreSQL database
- SQLAlchemy Async ORM
- Alembic database migrations
- JWT-based authentication
- OpenAI integration
- Dockerized development environment
- TypeScript frontend
- Tailwind CSS UI
- TanStack Query for API communication
- Python 3.13
- FastAPI
- SQLAlchemy 2.x (Async)
- Alembic
- PostgreSQL
- psycopg 3 (binary) — the driver, via
postgresql+psycopg:// - OpenAI SDK
- uv package manager
- Next.js 16
- React 19
- TypeScript
- Tailwind CSS 4
- TanStack Query
- shadcn/ui
- React Markdown
- JWT Decode
- Docker
- Docker Compose
.
├── backend
│ ├── alembic
│ ├── alembic.ini
│ ├── app
│ ├── Dockerfile
│ ├── entrypoint.sh
│ ├── __init__.py
│ ├── pyproject.toml
│ ├── README.md
│ ├── resources # CV markdown; system-prompt.md + contact.md gitignored
│ └── uv.lock
├── caddy
│ └── Dockerfile # Caddy + rate-limit module, built via xcaddy
├── Caddyfile # prod only; site address comes from $SITE_DOMAIN
├── docker-compose.yaml # dev
├── docker-compose.prod.yaml # production (Caddy + services)
├── docs
│ ├── architecture.md
│ ├── database.md
│ ├── deployment.md
│ └── development.md
├── frontend
│ ├── components.json
│ ├── Dockerfile
│ ├── eslint.config.mjs
│ ├── next.config.ts
│ ├── package.json
│ ├── package-lock.json
│ ├── postcss.config.mjs
│ ├── public
│ ├── README.md
│ ├── src
│ └── tsconfig.json
├── CHANGES.md
└── README.md
┌─────────────────────┐
│ Browser │
└──────────┬──────────┘
│ HTTPS
▼
Caddy (TLS)
│
┌──────────────┴──────────────┐
│ │
▼ ▼
Next.js Frontend FastAPI Backend (/api/*)
│
┌─────────────┴─────────────┐
▼ ▼
PostgreSQL OpenAI API
Before running the project, install:
- Docker
- Docker Compose
- Git
For local Python development outside Docker:
- Python 3.13
- uv
Clone the repository:
git clone https://github.com/buzzgreyday/workchat.git
cd workchatCreate the environment file. Both compose files read backend/.env and
interpolate ${POSTGRES_USER} and friends from a .env beside the compose
file, so the symlink is required in development too — without it every
docker compose command aborts before starting anything:
cp backend/.env.example backend/.env
ln -s backend/.env .envAdd your OPENAI_API_KEY to backend/.env, then create the system prompt —
the backend raises at import if it is missing or empty:
cp backend/resources/system-prompt.md.example backend/resources/system-prompt.mdBuild the containers:
docker compose buildStart all services:
docker compose up -dVerify that everything is running:
docker compose psView backend logs:
docker compose logs -f backendView frontend logs:
docker compose logs -f frontendStop all services:
docker compose downcd backend && uv run --env-file .env -m app.build_indexThe application uses environment variables for configuration. Secrets are never
committed — .env files are gitignored, and only .env.example files are tracked.
In production the only real env file is backend/.env, read by three different
consumers:
| Consumer | How it reads the file |
|---|---|
| Backend outside Docker (local dev) | load_dotenv(BACKEND_DIR / ".env") in app/common/config.py |
The backend and db containers |
env_file: ./backend/.env in both compose files |
Compose itself, for ${POSTGRES_USER} etc. in docker-compose.prod.yaml |
only ever reads a .env sitting next to the compose file |
The third consumer is why a repo-root .env also has to exist. Rather than
duplicating the values, symlink it so there is a single source of truth:
ln -s backend/.env .envWithout that symlink every docker compose command fails with
required variable POSTGRES_DB is missing a value, unless you pass
--env-file backend/.env by hand each time.
To set up a new deployment, copy backend/.env.production.example to
backend/.env, fill in real secrets, then create the symlink.
The frontend needs no env file of its own: the only variable it reads is
NEXT_PUBLIC_API_URL, supplied by environment: in docker-compose.yaml for
dev and baked in as a build arg in docker-compose.prod.yaml for production.
The backend is built with FastAPI and uses SQLAlchemy's asynchronous ORM together with PostgreSQL.
The Docker image:
- Uses Python 3.13 Slim
- Installs dependencies with uv
- Creates an isolated virtual environment
- Runs as a non-root user
Start only the backend:
docker compose up backendOpen a shell:
docker compose exec backend bashThe frontend is built with Next.js 16 and React 19.
The Docker image:
- Uses Node.js Alpine
- Installs dependencies
- Builds the production bundle
- Starts the Next.js server
- Exposes port 3000
Start only the frontend:
docker compose up frontendOpen a shell:
docker compose exec frontend shThe project uses PostgreSQL with Alembic for schema migrations.
Whenever SQLAlchemy models change. Not on a new deployment — the migrations
are committed and backend/entrypoint.sh already runs alembic upgrade head at
startup, so a fresh deploy needs nothing here:
docker compose exec backend alembic revision --autogenerate -m "migration description"Example:
docker compose exec backend alembic revision --autogenerate -m "add users and tokens"Always review generated migrations before applying them.
Migrations will be performed automatically when docker backend container is started, to set latest.
docker compose exec backend alembic upgrade headdocker compose exec backend alembic currentdocker compose exec backend alembic historyExecuted command in the "db" docker container.
-U for username, e.g. "postgres"
-d for database schema, e.g. "db"
docker compose exec db psql -U postgres -d dbList tables:
\dtExit PostgreSQL:
\qOnce the backend is running:
Swagger UI:
http://localhost:8000/docs
Redoc UI:
http://localhost:8000/redoc
OpenAPI Specification:
http://localhost:8000/openapi.json
Build containers:
docker compose buildRebuild backend:
docker compose build --no-cache backendRestart backend:
docker compose restart backendRestart frontend:
docker compose restart frontendRestart all services:
docker compose restartStop services:
docker compose stopRemove containers and volumes:
docker compose down -vView logs:
docker compose logs -fTypical development workflow:
- Update application code. Both trees are bind-mounted and both live-reload —
the frontend via
npm run dev, the backend viauvicorn --reload(dev compose only). No rebuild is needed for source edits. The backend watcher is scoped tobackend/app/, so changes underbackend/alembic/need adocker compose restart backend. - If SQLAlchemy models changed:
- Generate a migration —
docker compose exec backend alembic revision --autogenerate -m "…". - Review it. Autogenerate misses things like server defaults and renames.
- Apply it —
docker compose exec backend alembic upgrade head.
- Generate a migration —
- If you edited anything in
backend/resources/, rebuild the search index, or/chatwill keep answering from the old one:docker compose run --rm backend python -m app.build_index - Run the tests on the host —
cd backend && uv run pytest(see Testing). - Rebuild only when dependencies change —
docker compose build backendafterpyproject.toml/uv.lock, orfrontendafterpackage.json. - Commit code and migration files together, so a checkout never has models and schema out of step.
Run on the host, not in the container — the image is built with
uv sync --frozen --no-dev and pytest is in the dev dependency group, so it
is not installed inside either the dev or prod image:
cd backend && uv sync && uv run pytestLint the frontend:
docker compose exec frontend npm run lintdocker compose logs backendThe two most common causes are configuration, not code:
system prompt not found/is empty—backend/resources/system-prompt.mdis gitignored and read at import, so the process exits before serving.cp backend/resources/system-prompt.md.example backend/resources/system-prompt.md<VAR> not found or empty in environment variables—backend/.envis missing a required key. Compare it againstbackend/.env.example.
If docker compose itself aborts with required variable POSTGRES_DB is missing a value before any container starts, the root .env symlink is
missing: ln -s backend/.env .env.
Verify the database container is running:
docker compose psVerify the current migration:
docker compose exec backend alembic currentApply pending migrations:
docker compose exec backend alembic upgrade headBackend:
docker compose build --no-cache backendFrontend:
docker compose build --no-cache frontendCaddy handles TLS via Let's Encrypt and proxies /api/* to the backend,
everything else to the Next.js frontend. Alembic migrations run automatically
in the backend entrypoint, so a fresh database needs no manual step.
→ docs/deployment.md is the single source of truth
for deploying: prerequisites, the ordered first-deployment checklist, issuing
access tokens, logs and backups.
Possible future enhancements include:
- Move the hardcoded conditional prompts from the code to seperate files
- Resource search scoring
- Handle max queries reached, invalid/expired token, etc. in frontend
- Token persistence
- CI/CD pipeline
- Automated testing
- Background workers
- Monitoring and metrics
- Centralized logging
- Backup automation
- API versioning
- Chat client polymorph and factory (YAGNI)