10 KiB
WhereWoof — Return-Tag Platform
Branding (2026-08): "WhereWoof" → "Where Woof" (play on werewolf — dog-themed wolf). Page title: "Where Woof !". Future logo concept follows the werewolf wordplay (dog/wolf). Phase 2 change includes the rename task (headings, title, templates).
WhereWoof is a return-item tag platform (PetHub / ReturnMe style). Physical tags are pre-coded with a unique QR code and NFC chip, each pointing to its own URL. A finder scans the tag on a lost item (dog, baggage, skis…) and immediately sees how to return it.
This is NOT a location/GPS tracker. The tag has no electronics beyond the printed code and NFC. The system carries the return details, and — only when a finder permits — sends the finder's location to the owner so they can collect their item.
How it works — customer journey
- Buy — user buys a tag (pre-coded, unique
tag_codeprinted on QR + NFC). - First scan / first visit —
https://where-woof.com/t/<tag_code>shows "This tag is not set up yet." - Set up — the user creates an account or logs in → adds the tag to their account → fills in the item details.
- Details form — item type (dog / cat / baggage / skis / other), description, photo, phone number, address, notes.
- Done — scanning the tag from now on shows the return details.
- Edit — the tag page shows an edit icon for a logged-in owner → account CRUD page. The account page is also reachable from the main site (
where-woof.com).
Scan flow — finder experience (front-end)
When someone finds a lost item and scans the tag:
- Page loads the return details: item type, description, photo, owner contact.
- Geolocation prompt (browser permission). If granted, the location is sent immediately to the server → SMS alert to the owner with the coordinates. If rejected, the page still works fully.
- Ring owner —
tel:link that dials the owner's number directly. - Re-check location — button to re-request the finder's location (in case they changed their mind).
- Send SMS —
sms:link with a prefilled message asking the finder for their phone number (opens their SMS app). - Finder contact input — the finder can type their mobile number; it is forwarded to the owner (not stored long-term).
- The location button is hidden once location has been shared.
⚠️ Auto-reading the finder's phone number is NOT possible on the web — there is no browser API for it (privacy restriction; only native apps can). Manual input only.
Architecture — two systems, one shared Postgres
| System | Stack | Port / DNS | Audience | Purpose |
|---|---|---|---|---|
| Front-end | GOAT: Go + HTMX + Alpine.js + Tailwind + Postgres | .13:3020 → where-woof.com |
Finders + owners | Public tag pages, scan flow, account setup/CRUD |
| Admin | Laravel + Filament/Cashier + Postgres | .13:3030 → admin.where-woof.com |
Staff (Sam) | Users, tags, plans, billing, system management |
One shared Postgres database. Both apps are peers over it — the Go app reads/writes directly (via pgx / sqlc), Laravel manages the same schema, and Laravel owns migrations as the long-term convention. (Until Laravel exists, a canonical db/schema.sql is the source of truth.)
Why this design:
- The GOAT stack is a skills-update exercise — the point is Go + HTMX + Alpine + Tailwind as the front-end stack. The "S" (SQLite) from the original AI-written plan is swappable and is dropped in favour of Postgres: a multi-tenant product needs one real database, not hundreds of per-user SQLite files (no cross-user queries, migration/backup nightmare, Laravel can't query them).
- Dev Postgres already available locally: Supabase Postgres on
.27:5434. - Both apps sharing one DB means no sync layer and one backup.
Domains:
- Live site at InMotion Hosting redirects →
where-woof.com→ .35 (Caddy) → .13 (apps). admin.where-woof.com→ .35 → .13:3030.
Database schema (v2)
CREATE TABLE users (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
name TEXT,
phone TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE tags (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tag_code TEXT NOT NULL UNIQUE, -- printed on QR + NFC, public identifier
owner_id BIGINT REFERENCES users(id),
status TEXT NOT NULL DEFAULT 'unset'
CHECK (status IN ('unset', 'active', 'suspended')),
item_type TEXT CHECK (item_type IN ('dog', 'cat', 'baggage', 'skis', 'other')),
description TEXT,
photo_url TEXT,
phone TEXT, -- owner contact phone (shown via tel:)
address TEXT,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE scans (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tag_id BIGINT NOT NULL REFERENCES tags(id),
scanned_at TIMESTAMPTZ NOT NULL DEFAULT now(),
lat DOUBLE PRECISION,
lng DOUBLE PRECISION,
location_shared BOOLEAN NOT NULL DEFAULT FALSE,
scanner_phone TEXT,
alert_sent BOOLEAN NOT NULL DEFAULT FALSE
);
Plans / subscriptions tables come later with billing (Phase 5).
Accounts, plans & anti-abuse
- One account can hold 10–20 tags (limit enforced).
- Business model: annual fee + per-tag cost + max scans per plan.
- Scan limits are the anti-scam / cost protection — every scan can trigger an SMS (which costs us money), so caps prevent SMS-bombing and tag re-selling abuse.
- Tag status:
unset→active→suspended(admin can suspend). - Billing: Stripe (Australia) — NOT in the front-end phase; phased later.
⚠️ Preset tag IDs (anti-scam, confirmed 2026-08)
Tag codes are preset at manufacture (printed QR + NFC) and must match the database registry — there are no user-created tag codes. A scanned or claimed code that does not exist in the tags table is rejected ("tag not found / already claimed"). The 2014 system enforced the same rule via a productid_hash on the QR URL — "if productid_hash is not set we can do NO REGISTERING" — and tied each tag to a sold product (onlineorderproductid).
- The DB must be seeded with the real manufactured tag IDs (production list from the manufacturer, or recovered old IDs) — TEST codes are dev-only.
- Long-term (Laravel admin, Phase 4+): each tag is linked to a sold product/order for stronger validation (mirrors the old
products/online_order_product_detailsmodel). - Old product IDs from the 2014 system are not in the .13 archive (old live
websitebuilderDB not preserved) — recover from the old hosting account / manufacturer list, or generate fresh IDs.
Endpoints — GOAT front-end
| Method | Path | Purpose |
|---|---|---|
GET |
/ |
Main site home |
GET |
/t/:tag_code |
Public tag page — setup prompt or details + scan flow |
POST |
/t/:tag_code/scan |
Record scan + optional location → alert owner |
POST |
/t/:tag_code/contact |
Finder leaves their phone number for the owner |
GET/POST |
/register, /login, /logout |
Account auth (email + password, session cookie) |
GET |
/account |
Account CRUD: my tags |
POST |
/account/tags |
Add a tag to the account (enter tag code) |
GET/POST |
/account/tags/:id/edit |
Edit tag details |
POST |
/account/tags/:id/delete |
Remove tag |
Admin — Laravel (later phases)
User management, tag management (suspend/transfer), plans & billing (Stripe/Cashier), scan history & alerts dashboard, audit log. Not built in Phase 1.
Deployment
Dev (local on .27)
cd frontend && go run . --port 3020 # GOAT front-end
cd admin && php artisan serve --port 3030 # Laravel (later)
Build + push to .13
cd frontend
GOOS=linux GOARCH=amd64 go build -o where-woof .
rsync -avz where-woof templates/ static/ sam@192.168.20.13:/var/www/where-woof
Admin (later): rsync + composer install + php artisan migrate on .13.
Run on .13 (LIVE — Phase 7 deployed)
# systemd user service (enabled, auto-restart, survives reboot via loginctl enable-linger)
ssh sam@192.168.20.13
systemctl --user status where-woof # Active (running)
# unit: ~/.config/systemd/user/where-woof.service
# env: ~/.config/where-woof.env (chmod 600 — DATABASE_URL→localhost:5434, SESSION_SECRET, SMS_*)
# dir: /var/www/where-woof (binary + templates/ + static/uploads)
# ⚠ NixOS firewall on .13: port 3020 must be allowed externally
# (networking.firewall.allowedTCPPorts = [3020])
# Verify: curl localhost:3020 on .13 → 200; where-woof.home.lab → .35 → .13:3020 ✓
# DNS: where-woof.com A record must point at the router's public IP (like the lab domains) —
# currently still at InMotion; DNS/redirect fix is the outstanding user step.
Caddy (.35)
where-woof.com {
reverse_proxy 192.168.20.13:3020
}
where-woof.home.lab {
reverse_proxy 192.168.20.13:3020
}
admin.where-woof.com {
reverse_proxy 192.168.20.13:3030
}
DNS
- Live site (InMotion) → redirect / DNS update →
where-woof.com→ public DNS → .35 (Caddy) → .13. - Pi-hole:
where-woof.home.lab→ 192.168.20.35.
Roadmap
| Phase | What | Status |
|---|---|---|
| 1 | Front-end foundation: Go scaffold, Postgres schema, auth, tag setup, public tag page, account CRUD (GOAT) | ✅ |
| 2 | Scan flow: geolocation → SMS alert, tel:/sms: links, finder contact, re-check button |
✅ |
| 3 | HTMX / Alpine polish — inline edit, animations | ✅ |
| 4 | Laravel admin: users, tags, dashboard (same Postgres) | ✅ |
| 5 | Billing: Stripe (AU), plans, scan-limit enforcement, invoices | ⬜ |
| 6 | Photo uploads + object storage | ⬜ |
| 7 | Deploy front-end to .13, Caddy, DNS, InMotion redirect | ✅ (service live; DNS pending) |
| 8 | Deploy admin to .13 | ⬜ |
| 9 | Anti-abuse hardening: relay numbers, rate limits | ⬜ |
Future ideas
- NTFY ping to owner when a scan happens (alongside SMS).
- Tag transfer / reset for resale (admin feature).
- Relay number so the owner's real mobile isn't exposed to scanners.