# Hops n Grains Microbrewery A modern, full-stack craft microbrewery web application built with **TanStack Start** for the frontend and **PocketBase** for the backend, faithfully implementing the Google Stitch **Heritage & Harvest** tactile industrial design system. --- ## Architecture Overview ``` hops/ ├── backend/ │ ├── pocketbase # Linux AMD64 executable (WSL) │ ├── pocketbase.exe # Windows AMD64 executable (PowerShell) │ ├── pb_migrations/ # Database schema migrations & initial seeds │ │ └── 1788363300_initial_schema_and_seed.js │ └── pb_data/ # SQLite database & data storage ├── frontend/ # TanStack Start application │ ├── src/ │ │ ├── routes/ │ │ │ ├── __root.tsx # Root layout, fonts, nav, footer, reservation context │ │ │ ├── index.tsx # Home page (Google Stitch cinematic hero & craft overview) │ │ │ ├── brews.tsx # Craft Brews page (Google Stitch menu with IBU/ABV filters) │ │ │ ├── food.tsx # Global Comfort Food menu with beer pairing chips │ │ │ ├── brewery.tsx # The Brewery & Heritage story and locations │ │ │ └── live.tsx # Live On Tap draft board with keg telemetry │ │ ├── components/ │ │ │ ├── Navbar.tsx # Frosted glass top bar with mobile menu drawer │ │ │ ├── Footer.tsx # Dark industrial footer with newsletter signup │ │ │ ├── BeerCard.tsx # Tactile craft beer card with IBU/ABV meters & pairings │ │ │ ├── FoodCard.tsx # Food card with veg/non-veg tags & beer pairing │ │ │ ├── ReservationModal.tsx # Interactive table reservation modal connected to PocketBase │ │ │ └── TapHandle.tsx # Custom copper brewing handle slider from DESIGN.md │ │ ├── lib/ │ │ │ └── pocketbase.ts # PocketBase SDK client with fallback data resilience │ │ └── styles.css # Tailwind theme tokens, Playfair Display, Montserrat, textures │ └── package.json ├── ansible/ # Deployment playbook (roles: register, hops, caddy) ├── Dockerfile # Multi-stage production container build ├── .containerignore # Build-context excludes (Podman/Buildah) ├── .gitignore # Git repository ignore rules ├── docker-entrypoint.sh # Process supervisor running PB & Nitro ├── Makefile # Podman build targets (build-production / build-check) ├── deploy.sh # Build & push image, then run the Ansible deploy ├── package.json # Monorepo management scripts └── README.md ``` --- ## Running Locally ### 1. Start Both Frontend & Backend Concurrently ```bash npm run dev ``` Or start them individually: **Backend (PocketBase):** ```bash # In WSL / Linux: cd backend && ./pocketbase serve --http 127.0.0.1:8090 # In Windows PowerShell: cd backend ; .\pocketbase.exe serve --http 127.0.0.1:8090 ``` - API Endpoint: `http://127.0.0.1:8090` - PocketBase Admin UI: `http://127.0.0.1:8090/_/` - **Email:** `admin@hopsngrains.com` - **Password:** `HopsPassword1234!` **Frontend (TanStack Start):** ```bash npm run dev:frontend ``` - Web Application: `http://localhost:3000` --- ## Deploying The image is built with **Podman/Buildah** (not Docker) from the committed tree (`git archive HEAD`) and tagged with its **commit id**, so what runs on the host is exactly what git knows about. `deploy.sh` pushes the commit first, refuses a dirty tracked tree and requires `HEAD == origin/main`. Log in to the registry once per machine: `podman login registry.tanshu.com`. ```bash ./deploy.sh # push, build , deploy it to emily ./deploy.sh --no-push # skip the push (you already pushed) ./deploy.sh --force # skip the guards ``` Or run the pieces yourself: ```bash make build-production # podman build HEAD, push : + :latest make build-check # compile check from HEAD, no push cd ansible && ansible-playbook -u tanshu playbook.yml -e "image_tag=$(git rev-parse --short HEAD)" ``` The image is built for **linux/arm64** by default because `emily` is aarch64. Podman has no built-in QEMU, so cross-building from an x86_64 controller needs binfmt there (`qemu-user-binfmt`); override with `PLATFORM=linux/amd64`. To run the production image locally instead: ```bash podman build -t localhost/hops:dev . podman run --rm -p 3000:3000 -p 8090:8090 \ -e PB_ADMIN_EMAIL=admin@hopsngrains.com -e PB_ADMIN_PASSWORD=HopsPassword1234! \ -v hops_dev_data:/app/pb_data localhost/hops:dev ``` ### Ansible deployment Requirements on the controller: `ansible` (no collections — the play uses built-in modules plus the `podman` / `systemctl --user` CLI). The target host must already run the fleet's shared Caddy **rootless Quadlet** setup: the `caddy/playbook.yml` from the `ansible-playbooks` repo owns the Caddy unit, the rootless prerequisites (Podman, `loginctl enable-linger`, the low-port sysctl) and the Caddyfile this play appends to. The play targets the inventory host `emily` (present in `/etc/ansible/hosts`), connects as the unprivileged container user and **does not `become`**: the Quadlet units belong to that user, and the SSH connection's PAM session is what makes `systemctl --user` work. ```bash cd ansible ansible-playbook -u tanshu playbook.yml -e "image_tag=" ``` All deployment variables live in `ansible/vars/default.yml` (registry, image tag, domains, PocketBase admin credentials). The play: 1. Writes `~/.config/containers/systemd/hops.network` (`NetworkName=hops_net`) and a drop-in that attaches the shared Caddy unit to it, so Caddy can resolve `hops` by container name. 2. Logs in to the private registry, pulls `registry.tanshu.com/hops:{image_tag}` (rollback is the same play with an older commit), writes `~/podman-data/hops/.env`, installs `~/.config/containers/systemd/hops.container`, starts it as a **systemd user service** and waits for the image's healthcheck (PocketBase `/api/health` + the site). 3. Appends the site blocks to the shared Caddyfile (`~/podman-data/caddy/conf/Caddyfile`) and reloads Caddy: - `www.hopsngrains.com` (+ apex `hopsngrains.com`) — frontend on port 3000, PocketBase API/admin proxied under `/api/*` and `/_/*` - `admin.hopsngrains.com` — redirects to the PocketBase admin UI Nothing is published on the host; Caddy reaches the container over `hops_net`. The container runs as uid 0, so the unit maps it onto the podman user with `UserNS=keep-id:uid=0,gid=0`: `~/podman-data/hops` stays owned by `tanshu`, and PocketBase's files need no `podman unshare` to inspect or back up. DNS records for all three hosts must point at `emily`; Caddy obtains TLS certificates automatically on first request. This deploy starts from empty data — the schema migration in `backend/pb_migrations/` is applied on first start, so the seed beers and food are recreated automatically. --- ## Features & Pages | Route | Name | Highlights | |---|---|---| | `/` | **Home** | Cinematic hero matching Google Stitch mockup, brewery pillars, signature craft beer preview, call-to-action buttons. | | `/brews` | **Our Brews** | Interactive craft beer catalog matching Stitch design, style filters (Hefeweizen, Lager, IPA, Dark Ale, Cider), and custom copper **Tap Handle** IBU slider. | | `/food` | **Global Comfort Food** | Handcrafted dining menu (Wood-fired pizzas, smash burgers, truffle fries) categorized with recommended craft beer pairings and veg toggles. | | `/brewery` | **The Brewery** | 4-step industrial brewing science walkthrough (Mashing, Boiling, Fermentation, Cellar Pouring), and taproom details for Chandigarh & Panchkula. | | `/live` | **Live On Tap** | Real-time draft board tracking active tap numbers, ABV, IBU, and live remaining keg volume percentages. | | **Modal** | **Table Reservation** | Interactive booking form for Sector 9 Chandigarh or Sector 5 Panchkula, submitting live to PocketBase. |