Files
where_woof/where_woof.md

10 KiB
Raw Blame History

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

  1. Buy — user buys a tag (pre-coded, unique tag_code printed on QR + NFC).
  2. First scan / first visithttps://where-woof.com/t/<tag_code> shows "This tag is not set up yet."
  3. Set up — the user creates an account or logs in → adds the tag to their account → fills in the item details.
  4. Details form — item type (dog / cat / baggage / skis / other), description, photo, phone number, address, notes.
  5. Done — scanning the tag from now on shows the return details.
  6. 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:

  1. Page loads the return details: item type, description, photo, owner contact.
  2. 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.
  3. Ring ownertel: link that dials the owner's number directly.
  4. Re-check location — button to re-request the finder's location (in case they changed their mind).
  5. Send SMSsms: link with a prefilled message asking the finder for their phone number (opens their SMS app).
  6. Finder contact input — the finder can type their mobile number; it is forwarded to the owner (not stored long-term).
  7. 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:3020where-woof.com Finders + owners Public tag pages, scan flow, account setup/CRUD
Admin Laravel + Filament/Cashier + Postgres .13:3031admin.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:3031 (3030 = Langfuse).

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 1020 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: unsetactivesuspended (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_details model).
  • Old product IDs from the 2014 system are not in the .13 archive (old live websitebuilder DB 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 3031   # 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:3031
}

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 🟡 mechanism done (paid-gating + limits, no gateway)
6 Photo uploads + object storage (MinIO on .13)
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.