2026-06-26 08:00:18 +00:00
2026-06-25 01:59:09 +00:00
2026-06-11 22:34:55 -07:00
2026-06-11 22:42:35 +00:00
2026-06-26 08:00:18 +00:00
2026-06-10 18:34:23 +00:00
2026-06-19 22:55:00 -07:00
2026-06-18 08:29:03 +00:00
2026-06-10 18:34:23 +00:00
2026-06-19 22:55:00 -07:00
2026-06-19 22:55:00 -07:00
2026-06-19 22:55:00 -07:00
2026-06-19 22:55:00 -07:00
2026-06-19 17:37:19 +00:00
2026-06-10 18:34:23 +00:00
2026-06-10 18:34:23 +00:00
2026-06-10 18:34:23 +00:00
2026-06-11 22:34:55 -07:00

KMountain Flower Radio Station

A three-tier web application for a community radio station: Angular 21 frontend, FastAPI backend, PostgreSQL database.

Renders a hero page, about page, broadcast schedule, donation tiers, upcoming events, and a contact form — styled with a warm mountain/nature aesthetic.

Quick Start

Open the repo in VS Code and click Reopen in Container (or run Dev Containers: Reopen in Container from the command palette). The dev container starts both the backend (port 8000) and frontend (port 4200) automatically.

Open http://localhost:4200.

Option B — Manual (frontend + backend separately)

# Backend (uses SQLite for local dev — no database server needed)
cd backend
pip install -r requirements.txt
export KMTN_DATABASE_URL="sqlite+aiosqlite:///./kmountain.db"
uvicorn app.main:app --reload

# Frontend (new terminal)
npm install && ng serve

Open http://localhost:4200 and ensure the backend is running on http://localhost:8000.

Prerequisites

Requirement Version Notes
Node.js 20+ For frontend development
npm 11+
Python 3.12+ For backend development
PostgreSQL 16+ For production only (dev uses SQLite)

Architecture

                    ┌──────────────┐
                    │    Browser   │
                    │   :4200 / :80│
                    └──────┬───────┘
                           │
              ┌────────────┴────────────┐
              │                         │
         ┌────▼─────┐            ┌──────▼─────┐
         │  Nginx   │   /api/*   │  FastAPI   │
         │ (static) │─ proxy ──▶ │  :8000     │
         └──────────┘            └──────┬─────┘
                                        │
                                 ┌──────▼─────┐
                                 │ PostgreSQL │
                                 │   :5432    │
                                 └────────────┘

Three tiers:

  1. Frontend — Angular 21 SPA. In production, served by Nginx. In development, served by the Angular dev server on port 4200.
  2. Backend — Python FastAPI with async SQLAlchemy. Provides CRUD REST endpoints for programs (broadcast schedule), events (upcoming events), and donation tiers. Auto-generates Swagger/OpenAPI docs at /docs.
  3. Database — PostgreSQL 16. Schema contains three tables: programs, events, donation_tiers. Initial data is loaded via seed.py. Local development uses SQLite for convenience.

Project Structure

.
├── docker-compose.yml          # Dev compose (builds from source)
├── docker-compose.prod.yml     # Prod override (applied with -f docker-compose.yml -f docker-compose.prod.yml)
├── docker-compose.dev.yml      # Dev-sidecar compose (dev container)
├── Dockerfile                  # Frontend: Node build stage + Nginx serve stage
├── docker-entrypoint.sh        # Renders nginx config + config.json from env vars
├── nginx.conf.template         # Nginx config template (envsubst)
├── config.json.template        # Runtime API config template (envsubst)
├── .devcontainer/              # VS Code dev container
│   ├── Dockerfile              # Alpine Python 3.12 + Node.js + Angular CLI
│   └── devcontainer.json       # Dev container config
│
├── src/                        # Angular 21 frontend
│   ├── app/                    # Standalone components
│   │   ├── app.routes.ts       # Lazy-loaded route map
│   │   ├── app.config.ts       # App providers + APP_INITIALIZER (loads config.json)
│   │   ├── hero/ about/ schedule/ donate/ events/ contact/ navbar/ footer/
│   │   ├── services/           # HTTP services (program, event, tier, app-config)
│   │   └── interfaces/         # TypeScript types (Program, Event, Tier)
│   ├── environments/           # dev/prod config (apiBaseUrl)
│   └── styles/                 # SCSS design tokens
│       ├── _variables.scss     # Colors, spacing, breakpoints
│       ├── _mixins.scss        # Card, button, responsive mixins
│       └── _themes.scss        # SCSS → CSS custom properties
│
└── backend/                    # FastAPI backend
    ├── Dockerfile              # Python 3.12-slim + gunicorn/uvicorn
    ├── requirements.txt
    ├── seed.py                 # Initial data for all models
    └── app/
        ├── main.py             # FastAPI app, CORS, router registration
        ├── config.py           # ENV-based settings (KMTN_* prefix)
        ├── database.py         # async engine + session factory
        ├── models.py           # SQLAlchemy ORM (Program, Event, DonationTier)
        ├── schemas.py          # Pydantic schemas (Create/Update/Response)
        └── api/
            ├── programs.py     # CRUD /api/programs
            ├── events.py       # CRUD /api/events
            └── tiers.py        # CRUD /api/tiers

Local Development

Open the repo in VS Code → Reopen in Container. The dev container starts both services automatically. See .devcontainer/ for the configuration.

Manual Development

Run both layers in parallel — development uses SQLite (no database server needed):

# 1. Backend (new terminal)
cd backend
pip install -r requirements.txt
export KMTN_DATABASE_URL="sqlite+aiosqlite:///./kmountain.db"
export KMTN_CORS_ORIGINS='["http://localhost:4200"]'
uvicorn app.main:app --reload

# 2. Frontend (another terminal)
npm install
ng serve

The database auto-seeds on first startup. To reset and re-seed at any time:

curl -X POST http://localhost:8000/api/admin/reset

Setting up the bootstrap admin user

The backend supports a hardcoded "bootstrap admin" login via two environment variables. When both are set, anyone with those credentials can authenticate at POST /api/auth/login and receives a JWT token with admin privileges.

# Enable bootstrap admin login
export KMTN_ADMIN_USERNAME="admin"
export KMTN_ADMIN_PASSWORD="my-secret-password"

To log in, send a POST request to /api/auth/login:

curl -X POST http://localhost:8000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "my-secret-password"}'

This returns a JWT access token you can use to access protected endpoints:

curl http://localhost:8000/api/auth/me \
  -H "Authorization: Bearer <access_token>"

If either variable is left empty (the default), login is disabled and returns a 503 error. For production, also set KMTN_JWT_SECRET_KEY to a strong random value.

Configuration

Variable Default Where Description
Variable Default Where Description
--- --- --- ---
KMTN_DATABASE_URL postgresql+asyncpg://... (prod) / sqlite+aiosqlite:///./kmountain.db (dev) backend/app/config.py Database connection string
KMTN_CORS_ORIGINS ["http://localhost:4200"] backend/app/config.py CORS allowlist (JSON array)
KMTN_ENV development backend/app/config.py Set to production to disable admin endpoints
KMTN_ADMIN_USERNAME (empty) backend/app/config.py Bootstrap admin username. Leave empty to disable login.
KMTN_ADMIN_PASSWORD (empty) backend/app/config.py Bootstrap admin password. Both username and password must be set to enable login.
KMTN_JWT_SECRET_KEY change-me-in-production backend/app/config.py Secret key used to sign and verify JWT tokens. If leaked, anyone can forge admin tokens. Change in production.
API_UPSTREAM http://api:8000 Dockerfile Internal API target for Nginx proxy (Docker DNS)
API_PUBLIC_URL "" (empty) Dockerfile Browser-facing API URL. Empty = proxy mode; set to direct URL when ingress breaks proxy chain

Environment variables are prefixed with KMTN_ in the backend. For production, set KMTN_DATABASE_URL to your managed PostgreSQL connection and update KMTN_CORS_ORIGINS to your actual domain.

Building

# Frontend (produces dist/radio-station/browser/)
ng build --configuration production

For production deployment, serve the built static files with Nginx and run the backend behind it.

Testing

ng test              # Angular + Vitest

The backend API is self-documented at http://localhost:8000/docs (Swagger UI). The database auto-seeds on first startup — visit that URL to interactively test endpoints.

API Reference

The backend exposes CRUD endpoints for three models:

Method Endpoint Description
GET /api/programs List all programs (?day=1..7 to filter by day of week)
GET /api/programs/:id Single program
POST /api/programs Create program
PUT /api/programs/:id Update program
DELETE /api/programs/:id Delete program
GET /api/events List events (?active=true to filter active events)
GET /api/events/:id Single event
POST /api/events Create event
PUT /api/events/:id Update event
DELETE /api/events/:id Delete event
GET /api/tiers List donation tiers (ordered by display_order)
GET /api/tiers/:id Single tier
POST /api/tiers Create tier
PUT /api/tiers/:id Update tier
DELETE /api/tiers/:id Delete tier

Full interactive API docs at http://localhost:8000/docs.

Deployment

The app ships as two Docker images — a frontend (Angular + Nginx) and a backend (FastAPI + gunicorn). Build, tag, and push them to your registry, then deploy with Docker Compose.

Build and push

# Frontend
docker build -t <registry>/kmtnflower:latest .
docker push <registry>/kmtnflower:latest

# Backend
docker build -t <registry>/kmtnflower-api:latest ./backend
docker push <registry>/kmtnflower-api:latest

Deploy

On the target machine, create a Docker Compose file (see docker-compose.test.yml for a reference) that pulls the images and sets the required environment variables:

services:
  db:
    image: docker.io/library/postgres:16-alpine
    environment:
      POSTGRES_DB: kmountain
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  api:
    image: <registry>/kmtnflower-api:latest
    environment:
      KMTN_DATABASE_URL: postgresql+asyncpg://postgres:postgres@db:5432/kmountain
      KMTN_CORS_ORIGINS: '["http://yourdomain.com"]'
      KMTN_ENV: production
      KMTN_ADMIN_USERNAME: admin
      KMTN_ADMIN_PASSWORD: change-me
      KMTN_JWT_SECRET_KEY: change-me-to-a-random-string
    depends_on:
      db:
        condition: service_healthy

  frontend:
    image: <registry>/kmtnflower:latest
    environment:
      API_UPSTREAM: http://api:8000
      API_PUBLIC_URL: ""
    ports:
      - "4200:80"
    depends_on:
      - api

volumes:
  pgdata:

Then start:

docker compose up -d

API_PUBLIC_URL

The frontend container renders a config.json at startup from the API_PUBLIC_URL environment variable. This controls how the Angular app reaches the API:

Mode Value When to use
Proxy (default) "" (empty) Browser calls /api/* relative paths; Nginx forwards to API_UPSTREAM. Use when no external ingress interferes.
Direct "http://yourhost:8000" Browser calls the API container directly. Use when an external ingress layer (e.g., TrueNAS middleware, Kubernetes ingress) breaks the Nginx proxy chain. Requires CORS to be configured on the backend.

Example: TrueNAS Scale

For a TrueNAS Scale deployment where the built-in ingress layer may redirect traffic:

services:
  db:
    image: docker.io/library/postgres:16-alpine
    environment:
      POSTGRES_DB: kmountain
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  api:
    image: truenas.local:35000/kmtnflower-api:latest
    environment:
      KMTN_DATABASE_URL: postgresql+asyncpg://postgres:postgres@db:5432/kmountain
      KMTN_CORS_ORIGINS: '["http://truenas.local:34200", "http://truenas.local"]'
      KMTN_ENV: production
      KMTN_ADMIN_USERNAME: admin
      KMTN_ADMIN_PASSWORD: change-me
      KMTN_JWT_SECRET_KEY: change-me-to-a-random-string
    ports:
      - "8000:8000"
    depends_on:
      db:
        condition: service_healthy

  frontend:
    image: truenas.local:35000/kmtnflower:latest
    environment:
      API_UPSTREAM: http://api:8000
      API_PUBLIC_URL: "http://truenas.local:8000"
    ports:
      - "34200:80"
    depends_on:
      - api

volumes:
  pgdata:

Note: The database auto-seeds with initial data on first startup. To re-seed at any time (dev only): curl -X POST http://localhost:8000/api/admin/reset.

Bare-metal production

For production, you'll need to set up:

  • A PostgreSQL database (managed or self-hosted)
  • The FastAPI backend (gunicorn/uvicorn)
  • Nginx to serve the Angular build and proxy /api/* to the backend
# 1. Build the frontend
ng build --configuration production

# 2. Configure the backend
export KMTN_DATABASE_URL="postgresql+asyncpg://user:pass@host:5432/kmountain"
export KMTN_CORS_ORIGINS='["https://yourdomain.com"]'
export KMTN_ENV="production"
export KMTN_ADMIN_USERNAME="admin"
export KMTN_ADMIN_PASSWORD="change-me"
export KMTN_JWT_SECRET_KEY="change-me-to-a-random-string"

# 3. Start the backend
cd backend
pip install -r requirements.txt
uvicorn app.main:app --host 0.0.0.0 --port 8000

# 4. Serve the frontend with Nginx (or your preferred web server),
#    pointing /api/* requests to http://localhost:8000

The database auto-seeds on first startup.

Production hardening checklist

  • Use a managed PostgreSQL database (RDS, Cloud SQL, etc.)
  • Update KMTN_CORS_ORIGINS to your actual domain (not localhost)
  • Set KMTN_ENV="production" to disable admin endpoints
  • Set KMTN_ADMIN_USERNAME and KMTN_ADMIN_PASSWORD to strong values
  • Set KMTN_JWT_SECRET_KEY to a strong random value
  • Add a reverse proxy (Caddy, Traefik) in front of Nginx for HTTPS
  • Set up log rotation
  • Configure a process manager (systemd, PM2) for the backend

Development Tools

  • Dev container — Open the repo in VS Code → "Reopen in Container" for a pre-configured environment with Angular CLI, Node.js, Python 3, and Git (.devcontainer/).
  • VS Code workspaceworkspace file in the repo root configures both Angular and Python extensions.
  • Prettier — Run npx prettier --write . to format the codebase.
  • Prettier config.prettierrc (100 char width, single quotes, Angular HTML parser).

Tech Stack

Layer Technology
Frontend Angular 21 (standalone components, signals-ready)
Backend FastAPI 0.115, Python 3.12
Database PostgreSQL 16 (async via SQLAlchemy 2.0)
Styling SCSS with design tokens (variables → mixins → CSS custom properties)
Routing Angular Router (lazy loaded) + Nginx SPA fallback
HTTP RxJS observables (frontend → FastAPI)
Containers Dev container (VS Code Remote - Containers)
CI/CD (not yet configured)
Testing Vitest (via Angular CLI)
S
Description
No description provided
Readme
8.3 MiB
Languages
Python 38.3%
TypeScript 28.3%
SCSS 17.4%
HTML 14.3%
Dockerfile 1.4%
Other 0.3%