# 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 ├── .dockerignore # Build-context excludes (podman and Docker) ├── .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 the frontend and backend together ```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 `deploy.sh` pushes the current branch, refuses a dirty tracked tree, and requires `HEAD == origin/main`. It builds that commit with podman and runs the Ansible play on emily. Log in to the registry once per machine: `podman login registry.tanshu.com`. ```bash ./deploy.sh # deploy HEAD ./deploy.sh --prune # also drop old images on emily, keep 3 ./deploy.sh --force # skip the guards ``` The image is tagged with its commit id and `:main`. Rollback runs the same play with an older `image_tag`: ```bash make build-production # podman build HEAD, push : and :main make build-check # build from HEAD, push nothing cd ansible && ansible-playbook playbook.yml -e "image_tag=$(git rev-parse --short HEAD)" ``` The image builds 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-static`). Override with `PLATFORMS=linux/amd64`: ```bash make build-production PLATFORMS=linux/amd64 ``` To run the production image locally: ```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 The controller needs `ansible`. The target host needs the shared Caddy rootless Quadlet setup from the `ansible-playbooks` repo. That playbook owns the Caddy unit, the rootless prerequisites, and the Caddyfile this play appends to. The play targets `emily`, connects as the unprivileged container user, and does not use `become`. The Quadlet units belong to that user, and the SSH session's PAM context makes `systemctl --user` work. ```bash cd ansible ansible-playbook playbook.yml -e "image_tag=" ``` Skip the Caddyfile update until DNS points at emily. Otherwise Caddy retries certificate issuance for a name that still resolves to monoco: ```bash ansible-playbook playbook.yml -e "image_tag=" --skip-tags caddy ``` Deployment variables live in `ansible/vars/default.yml`. The play: 1. Writes `~/.config/containers/systemd/hops.network` with `NetworkName=hops_net`, plus a drop-in that attaches the shared Caddy unit to it. Caddy can then resolve `hops` by container name. 2. Logs in to the registry, pulls `registry.tanshu.com/hops:{image_tag}`, writes `~/podman-data/hops/.env`, installs `hops.container`, starts it as a systemd user service, and waits for the healthcheck. 3. Appends the site blocks to `~/podman-data/caddy/conf/Caddyfile` and reloads Caddy. `www.hopsngrains.com` and the apex serve the frontend on port 3000 and proxy `/api/*` and `/_/*` to PocketBase. `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`. Files under `~/podman-data/hops` stay owned by `tanshu`, and PocketBase's files need no `podman unshare` to read or back up. DNS for all three hosts must point at emily before Caddy can issue certificates. The first deploy starts with empty data. PocketBase applies the schema migration in `backend/pb_migrations/` on first boot and recreates the seed beers and food. --- ## Features and 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. |