8.3 KiB
WhereWoof — Return-Tag Platform
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.
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
cd /var/www/where-woof && ./where-woof --port 3020
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 | ⬜ |
| 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.