Internal operations manual

Nudge Tags — the whole machine, on one page

Every domain, every login, every key, every scheduled job, every table in the database and every process that turns a sheet of acrylic into recurring revenue. Written so either founder can pick it up cold.

LAST FULL AUDIT 19 August 2026 OWNERS Gauresh Malhotra & Leon Allendes (50/50) ABN 64 539 870 400 PRESS / TO SEARCH

01 What the business actually is

Two products, sold together, in that order. The plaque gets you in the door; the subscription is the business.

2
Revenue lines
5
Live web surfaces
11
External services
10
Keys configured
4
Keys still missing
6
Things not registered
The hardware Shipping

A cast-acrylic plaque with a passive NFC chip on the back. A customer holds their phone near it and a page opens. No app, no battery, no camera, nothing to scan.

Round
Ø90 mm · 3 mm cast acrylic · NTAG215 or better · points at anything: menu, bookings, ordering, events
Square
100 × 100 mm · 3 mm cast acrylic · NTAG213 · the Google-review plaque, five brass stars
Finishes
Sand, Ink (black), Chalk (white), Forest (green)
Fixing
3M VHB full-base adhesive; Ø25 mm chip surface-stuck on the back, reads through the 3 mm face
Price
Regular $15 each, $10 at 10+. Custom $20 each, $15 at 10+. Plus a flat $50 install per job
The software Live

Nudge Reviews at app.nudgetags.com. Every plaque points at a link we own, so the destination can change without reprinting, every tap is counted, and the whole thing stops working the day they cancel.

Price
$149/mo per venue, or $99/mo billed yearly ($1,188). AUD.
Rule
Every hardware sale includes the subscription. Non-negotiable — it is the actual business.
Per venue
Reviews, menus, or both — same price. Team members are free.
Sold
In person only. Public sign-up is deliberately switched off.
Who owns it

A partnership between Gauresh Malhotra and Leon Allendes trading as Nudge Tags. 50/50. Explicitly not a Purple Pi Marketing property — no PPM name, colour or address may appear on anything Nudge-facing.

ABN
64 539 870 400 (the partnership's)
Contact
contact@nudgetags.com · 0481 118 358
Law
Queensland. Prices in AUD, exclusive of GST.
Not done
Business name not registered with ASIC; no Pty Ltd; Stripe not activated for live payments
The moat, in one sentence

Anyone can buy an NFC sticker for two dollars. What they cannot buy is a plaque whose destination you control, whose taps you can count per table and per hour, and which switches off when the invoice does. The sticker is the cheap part; the redirect layer and the dashboard are the product.

Repoint
Change where a plaque sends people, from the dashboard, without touching the plaque
Measure
Each plaque and each table has its own code, so taps split by placement, hour and day
Leverage
Cancellation pauses every link the venue has, immediately
Read this before anything else None of the source code is in version control. There is no git repository for the app, the website, the quote calculator or the print scripts — the only copies are this Mac (~/Documents/claude/nudge-tags/) and the Google Drive backup of that folder. A bad overwrite, a disk failure or a wrong rm loses work with no undo, and there is no history to see what changed between two deploys. Putting the repo on GitHub (private) is the single highest-value hour of maintenance available.

02 The whole process, start to finish

Three tracks run in parallel: making the plaque, the tap itself, and the money. Click any box for the detail behind it.

① MAKE — acrylic, chip, install Artworkbrand/*.py → Drive Spec sheetsent to the factory Supplier orderAlibaba · Shenzhen Write + lock chipNFC Tools app Install3M VHB, on site ② TAP — what happens in the two seconds after a phone touches the plaque Customer tapsno app, no camera Our link opens/r/slug?c=code Tap recordedvisits table They tap a starthreshold decides Google review boxat or above threshold Private feedbackbelow threshold The dashboardowner sees it all ③ MONEY — quote to books Quoteprice list + install Service Agreementsigned, per venue Account created/admin → setup link Logged to the booksGoogle Sheet /financeslive, superadmin
Click a box above.

03 Every address we own

Four things are live on the internet under this brand. Everything else is a redirect or a reserved name.

AddressWhat it isHosted bySource on diskWho can see itState
nudgetags.com
www → same
The public marketing site. 8 hand-written HTML pages with a live 3D plaque showroom. Vercel project nudgetags nudge-tags/website/ Anyone Live
app.nudgetags.com The product. Marketing front page, customer tap pages, the venue dashboard, the operator console, the finance dashboard and the new in-app quote builder. Vercel project nudge-reviews (Next.js, region syd1) nudge-tags/nudge-reviews/ Mixed — see the route table below Live
quote.nudgetags.com Secured 20 Aug 2026. 307-redirects on every path to app.nudgetags.com/quote, which is behind the admin login. The old public calculator no longer serves here. Vercel project nudge-tags-quote nudge-tags/quote/index.html Admin login only Secured
finances.nudgetags.com Not an app — a 307 redirect to app.nudgetags.com/finances, on every path. A shortcut, nothing more. Alias on nudge-reviews next.config.mjs Redirects to a login wall Live
nudge-reviews.vercel.app The old host. 307-redirects to app.nudgetags.com preserving the path, kept because a QR code printed against it may be on someone's counter. Deliberately temporary, not permanent — browsers cache permanent redirects hard, and this is the fallback if DNS on the custom domain ever breaks. Vercel next.config.mjs Anyone Kept on purpose
This page
nudge-tags-ops.vercel.app
The operations manual you are reading. Internal, noindexed. A subdomain (ops.nudgetags.com) is one GoDaddy CNAME away and is how Leon gets access without a Vercel account. Vercel project nudge-tags-ops nudge-tags/ops/ Anyone with the link Live
go.nudgetags.com Reserved for short plaque redirect links. No DNS record exists yet. Unused

DNS and email — where the domain actually points

Registrar & nameservers

GoDaddy. The nameservers are deliberately left at GoDaddy (ns39/ns40.domaincontrol.com) rather than moved to Vercel, because the domain carries the Google Workspace mail records — moving them would have taken the email down.

A @
76.76.21.21 → Vercel
CNAME www
nudgetags.com
CNAME app
cname.vercel-dns.com
CNAME finances
cname.vercel-dns.com
CNAME quote
Vercel (*.vercel-dns-017.com)
MX
Google Workspace (5 records, aspmx.l.google.com et al)
Mail

Google Workspace on nudgetags.com, admin account contact@nudgetags.com. In Gauresh's Chrome it is profile index 5 — go straight to admin.google.com/u/5/ rather than hunting through profiles.

Uses
Contact address on every page and the contract; the FormSubmit relay target; the intended Resend sender
Logo
Set to the full-name wordmark, 320×60
Open
Account avatar is still the grey letter tile — the upload needs a native file picker, so it has to be done by hand
Certificates and renewal

TLS is issued and renewed automatically by Vercel for every attached domain — nothing to diarise there. What does need a diary entry is the domain registration itself at GoDaddy; if it lapses, every surface above dies at once, including email.

Check
Auto-renew is on and the card on file is current
Warning
The marketing site sends HSTS with preload and includeSubDomains. If the domain is ever submitted to the browser preload list, every subdomain is locked to HTTPS in shipped browsers and it is very hard to reverse.

Every route on app.nudgetags.com

This is the complete list. Anything not here does not exist.

Open to the public — no login

RouteWhat it doesNotes
/Product marketing page with a live rating calculator and the pricing tablePrices shown here come from one constant in the code — changing it also changes the Terms page
/r/<slug>The plaque link. Records the tap, then shows the five starsNo JavaScript required. Never cached. Falls through to the menu for menu-only venues
/r/<slug>/rateReceives the star tap and routes to Google or the private formRate-limited: 6 per visitor per 5 min, 400 per venue per 5 min
/r/<slug>/feedbackSaves the private complaint and emails the ownerSaved before the email is attempted, so a mail failure never loses it
/m/<slug>A menu tap — records it and forwards straight to the menuBuilt but orphaned — nothing links to it. See the risk list
/w/<key>The embeddable ratings badge a venue puts on its own websiteAn iframe, deliberately not a script. Framing by anyone is intentional
/login /forgot /reset/<token>Sign in and password recoveryReset emails cannot currently be sent — see the keys register
/signupShows "accounts are set up with your plaques"Public sign-up is closed unless SIGNUP_OPEN=1
/join/<token>A staff member accepts a team inviteInvite valid 14 days, single use, revocable
/terms /privacy /refundThe product's legal pagesReal commitments — 30-day yearly refund, 90-day data retention
/api/stripe/webhookWhere Stripe reports paymentsRejects everything without a valid signature
/api/errorWhere the browser reports a crashUnauthenticated by design, 20 per IP per 5 min
/api/cron/places
/api/cron/report
The two scheduled jobsGuarded by CRON_SECRET

Signed in — a venue owner or their staff

RouteWhat the customer does hereWho
/onboardingFirst screen after setting a password: name the venue, paste the Google link, pick the thresholdOwner
/dashboardOverview — six tiles, the Google-rating line, activity chart, star spread, reviews-vs-menus split, per-venue table, per-placement tableEveryone
/dashboard/locationsThe reviews tab: the link, the QR, a phone preview, six charts, the star funnel, and the placements list where each plaque gets its own codeEveryone reads · owner edits
/dashboard/menusThe menu tab: the menu link and one code per table, added singly or as a rangeEveryone reads · owner edits
/dashboard/staffStaff leaderboard — each person gets their own code, ranked by ratings earned rather than tapsEveryone reads · owner edits
/dashboard/feedbackEvery private complaint, themed automatically, with mark-resolved and a CSV exportEveryone; members may resolve
/dashboard/settingsVenue switches, Maps link, timezone, the ratings badge, own profile and password, team invitesEveryone; venue and team edits are owner-only
/dashboard/billingPlan, checkout and the Stripe portalOwner only
/dashboard/setup/<id>Bolt a missing capability onto an existing venueOwner only
/api/google-linkConverts a pasted Maps link into a direct review link, live, before savingSigned in — deliberately, since it follows redirects server-side
/dashboard/locations/<id>/qr
/dashboard/menus/<id>/qr
Downloads the QR for one placement — 1024 px SVG, ink on white, so it prints crisply even at receipt size. Generated by our own encoder, so no third party ever sees a customer's linkSigned in, scoped to venues they may see
/dashboard/feedback/exportCSV of the private feedback inbox, up to 5,000 rowsSigned in
/logoutSigns outAnyone

Ours only — the operator surfaces

RouteWhat it isGate
/adminThe operator console. Create a customer account and get their 7-day setup link; see every business, its owner email, plan status, venue count and rating count; see the last 25 crashes and the 24-hour error countEmail listed in SUPERADMIN_EMAILS
/financesThe founder finance dashboard. Overview, Revenue, Expenses, Units & Inventory, Ledgers — live from the accounting sheetSame
/quoteThe in-app quote builder (new). Prices an order against real stock and real landed cost, then logs the closed sale straight into the Income tab of the accounting sheetSame
/api/financesThe JSON behind the finance dashboardSame — returns 403 otherwise
/api/quote/closeWrites a closed sale to the sheet. The browser never sees the sheet's write tokenSame
Two things to know about the operator surfaces A "superadmin" is not a role in the database — it is purely a list of email addresses in one Vercel environment variable. Whoever signs in with a listed address gets the operator console, the finance dashboard and the ability to permanently delete a venue. That also means a customer who changes their own email to a listed address would become one. Treat that variable as a credential.

Right now it holds only Gauresh's own addresses (the gmail and contact@nudgetags.com — worth re-reading the live value in Vercel before assuming). Leon cannot open /admin, /finances or /quote. There is no way to grant the finance dashboard without also granting the operator console over every customer account — if that split matters, it needs building.

04 Every URL, complete

Every address the business owns or answers on — public pages, customer links, operator-only screens, machine endpoints, redirects and consoles. If a URL exists and is not on this list, the list is wrong.

The complex ones — watch these Any URL with a [placeholder] in it is highlighted below. Those are the links that are built per venue, per placement or per deal rather than being fixed — which makes them the ones most likely to be wrong, and the ones worth checking first when something doesn't work. Each has a real worked example beside it. Where the example points at our own demo venue it is genuinely live, so you can click it and see exactly what a customer sees.
The rule Any time a route, subdomain, redirect or generated link is added or changed, it gets added here and to the links document in Drive, in the same session. A URL that lives only in a chat message or in someone's head is a URL that gets lost. Two of the entries below — the thank-you and welcome pages — are generated per customer and would never have surfaced without being written down.

nudgetags.com — the public marketing site

URLWhat it isAccess
nudgetags.comHome — the pitch in one scrollPublic
/plaquesThe 3D showroom and the full physical specPublic
/plaques?p=circle|square&c=sand|black|white|greenDeep link to one finish. Eight combinations — paste the exact one you are describing on a callPublic
/how-it-worksThe mechanics, the redirect moat, and the three honest limitsPublic
/softwareSells the subscription halfPublic
/faqObjections answered, in four groupsPublic
/contactEnquiry form · anchor #enquiry-form. Serves both a walkthrough booking and a custom-plaque quotePublic
/enquiry-receivedThe confirmation page. The form navigates here on success, so a completed enquiry is a real page view with a URL a pixel can fire on. NoindexedPublic
/terms · /privacyWebsite terms and the privacy noticePublic, noindexed
www.nudgetags.comRedirects to the bare domainPublic
/product · /products/plaques (permanent)Redirect
/pricing/contact. Plaque prices are never published; software prices are, on the app's own pricing pageRedirect
/reviews/software (temporary)Redirect

app.nudgetags.com — customer-facing

URLWhat it isAccess
app.nudgetags.comThe product's own marketing and pricing pagePublic
/r/<slug>The plaque link. This is what goes on a chipPublic
/r/<slug>?c=<code>One specific placement — a plaque, a table, a staff member. The ?c= code is the entire tracking mechanismPublic
/r/<slug>/rate · /feedbackThe star tap and the private complaint form (both POST)Public
/api/enquiryWhere the website contact form posts. POST + OPTIONS, CORS limited to nudgetags.com. Writes to enquiries and emails a notificationPublic endpoint
/demo/menuA sample menu for a fictional venue. Demo 2's table tags open this, so a demo never sends a prospect to a real unrelated business. NoindexedPublic, unlisted
/m/<slug>Menu tap — records it and forwards to the menuPublic
/w/<key>The embeddable ratings badge a venue pastes on its own site. Public by design, framed by anyone
e.g. app.nudgetags.com/w/e785ed17498a4f7a86b6 live — the demo venue's real badge
Public, unguessable key
/welcome/<business-slug>Generated when a close is logged with the dashboard included (the default). Shows their order and their review-page link
e.g. app.nudgetags.com/welcome/the-lark-cafe — illustrative; no close has been logged yet
Public, unlisted
/thanks/<business-slug>Generated on a hardware-only close. Order summary only — no workspace, no review link
e.g. app.nudgetags.com/thanks/the-lark-cafe — illustrative
Public, unlisted
/login · /forgot · /reset/<token> · /logoutSign in and password recovery. The reset link doubles as the 7-day setup link a new customer receives
e.g. app.nudgetags.com/reset/8a41f0c92b7e5d63a10f4c8b2e97d5061a3f8c24 — 40 hex characters, single use
Public
/signupSays accounts come with the plaques — self-signup is switched offPublic
/join/<token>A staff member accepts a team invite. 14 days, single use
e.g. app.nudgetags.com/join/3d7b1e5a9c04f28e6b7152ac8de60934f1b7a2c8 — shown once, never recoverable
Public, unguessable
/terms · /privacy · /refundThe product's legal pages — real commitmentsPublic
Forms that have no confirmation URL

Not every submission ends at a page, and the ones that don't still need writing down or nobody can review what the customer was told. Three of ours confirm in place, with no navigation and therefore no URL to visit:

FormWhat the customer sees on success
nudgetags.com/contactNow navigates to /enquiry-received. The inline line is only used on failure: “That did not send. Ring 0481 118 358 or email contact@nudgetags.com and we will pick it up straight away.”
/r/<slug>/rateEither a redirect to Google, or the feedback form rendered in place. A staff card always renders the feedback form, whatever the stars.
/r/<slug>/feedbackA thank-you rendered in place at the same URL — there is no /thank-you route for it.

The contact form now has one/enquiry-received, added 22 Aug 2026 for exactly this reason. The two review-flow forms still confirm in place, which is correct: they are customer moments, not conversions we would ever advertise against. GA4 is still not installed anywhere, so the confirmation page currently has nothing listening to it.

The two generated pages are worth understanding: the slug is the customer's name lowercased with anything non-alphanumeric turned into hyphens, capped at 48 characters — "The Lark Café" becomes /welcome/the-lark-caf. Each close's exact URL is also written onto its row in the Income sheet, so the link is recoverable from the books.

app.nudgetags.com — signed in

URLWhat it isAccess
/onboardingFirst screen after setting a passwordOwner
/dashboardOverviewOwner & team
/dashboard?loc=<id>Scoped to one venue — the workspace switcher writes this
e.g. app.nudgetags.com/dashboard?loc=529a176891554522b8e4 — the demo venue's real id
Owner & team
/dashboard/locations · /menus · /staff · /feedback · /settingsThe five working tabsOwner & team
/dashboard/billingPlan, checkout, Stripe portalOwner only
/dashboard/setup/<id>?add=reviews|menuBolt a missing capability onto a venue
e.g. app.nudgetags.com/dashboard/setup/529a176891554522b8e4?add=menu
Owner only
/dashboard/locations/<id>/qr?c=<code>
/dashboard/menus/<id>/qr?c=<code>
Downloads that placement's QR as a 1024 px SVG
e.g. …/locations/529a176891554522b8e4/qr?c=plaque-1
e.g. …/menus/7db6a67786a345359a99/qr?c=table-1 — both real ids
Owner & team
/dashboard/feedback/export?loc=<id>Private feedback as CSV
e.g. …/feedback/export?loc=529a176891554522b8e4
Owner & team

app.nudgetags.com — ours only

URLWhat it is
/adminOperator console. Create customer accounts, see every business, read the last 25 crashes
/financesThe real numbers, live from the accounting sheet
/finances?tab=overview|revenue|expenses|units-inventory|ledgers&range=all|12m|6m|3m|1m&q=…Deep link to one tab, range and search — survives a refresh and can be sent to the other founder
/quoteThe quote builder. Prices from the price list against real stock, logs the close to the books, generates the welcome page

Machine endpoints — nothing to click, but they exist

URLCalled byGuarded by
/api/stripe/webhookStripe, on every payment eventSignature check
/api/cron/placesVercel Cron, 05:00 Brisbane dailyCRON_SECRET
/api/cron/reportVercel Cron, 07:30 Brisbane on the 2ndCRON_SECRET
/api/errorThe browser, when a page crashesUnauthenticated, rate limited
/api/google-linkThe dashboard, converting a pasted Maps linkSigned-in session
/api/financesThe finance dashboard, every 60 secondsSuperadmin
/api/quote/closeThe quote builder, logging a saleSuperadmin

Subdomains and redirects

URLGoes toState
quote.nudgetags.com307 → app.nudgetags.com/quote on every path, landing on the admin login. The retired nudge-tags-quote project now serves only a no-pricing stub that redirects the same way, so neither URL exposes cost or marginSecured
finances.nudgetags.com307 → app.nudgetags.com/finances, on every pathLive
nudge-reviews.vercel.app307 → app.nudgetags.com, path preserved. Kept because a QR printed against it may be on a counterLive
nudge-tags-ops.vercel.appThis page. A subdomain (ops.nudgetags.com) is one CNAME awayLive
go.nudgetags.comReserved for short plaque links. No DNS record yetNot created
ops.nudgetags.comIntended home of this manualNot created

Updated 22 Aug 2026: no .vercel.app address serves anything any more. ops.nudgetags.com was created and the Operations Map moved onto it; nudge-tags-ops and nudgetags.vercel.app now 307 to the owned domain, joining nudge-reviews.vercel.app which already did. quote.nudgetags.com was moved off the retired quote project and its redirect now fires. Per-deployment URLs (<project>-<hash>-purple-pi-marketing.vercel.app) still resolve, embed the team name, and should never be sent to anyone.

Consoles — where to log in

WhatURL
Vercel — all four projectsvercel.com/purple-pi-marketing
Supabase — the databasesupabase.com/dashboard/project/nlpqsgqrixerlsqhmkms
Stripe — sandbox only todaydashboard.stripe.com
Google Workspace adminadmin.google.com/u/5/
Google Apps Script — the finance feedscript.google.com
GoDaddy — domain and DNSdcc.godaddy.com/control/dnsmanagement?domainName=nudgetags.com
Google Cloud — for the Places keyconsole.cloud.google.com (no project created yet)
Resend — emailno account yet
The accounting sheetDrive › Nudge Tags › Accounting
The enquiry-form relay (retired 22 Aug 2026 — replaced by our own /api/enquiry)formsubmit.co/ajax/contact@nudgetags.com

05 Google — what it does, when it costs money, and what to check

There is no single "Google API". There are five separate touchpoints, and only three of them cost anything. The one that earns the money is free.

The important thing first No venue ever connects a Google account. There is no sign-in-with-Google, no OAuth, no Google Business Profile permission to request. Onboarding a venue needs nothing from Google's side — the owner pastes a Maps link and we do the rest. That is deliberate: it removes the single biggest source of friction in this category, and it means nothing can be revoked by a customer's IT person later.

The five touchpoints

#WhatUsed forCosts money?Working today?
1 The write-a-review deep link
no API at all
The money mechanic. Sends a happy customer into Google's "write a review" box with it already open, instead of the listing page where nobody finds the button. Free — it is just a URL Yes
2 Places API (New) — Text Search
places:searchText
Looks a venue up once to store its address, coordinates and photo reference, so the dashboard card shows where the place is. Yes, per lookup No key
3 Places API (New) — Place Details
places/<id>
"What is this venue's public star rating today?" Runs once per venue per day and builds the rating-over-time line — the chart that proves the plaques worked. Yes, and this one is the expensive tier No key
4 Street View Static (with Places Photo as fallback) The shopfront thumbnail on the venue cards. Street View first because it is an actual photo of the building; user-uploaded Places photos are more often a plate of food. Yes, per image No key
5 Google Apps Script over a Google Sheet The accounting feed — how the finance dashboard reads the books and how the quote builder writes a closed sale back. Free (Workspace quota) Yes

How the free part works — the bit that actually earns

Every Google Maps URL for a place carries a feature id that looks like !1s0x6b91598d0ef1ec8f:0x5177853dd7667eae. Dropped into a specific URL shape with the action code !12e1, that opens the review composer directly:

https://www.google.com/maps/place//data=!4m3!3m2!1s<FEATURE-ID>!12e1
                              ↑
                    the empty name segment is required — do not "tidy" it away

When an owner pastes a link, the app runs this ladder:

  1. Is it Google at all?

    The hostname must be one of g.page, goo.gl, maps.app.goo.gl, google.com, search.google.com, maps.google.com, business.google.com. Anything else is refused with "That link isn't a Google one."

  2. Is it already a review link?

    If it contains /writereview, /review or !12e1, it is left exactly as it is. A g.page/r/…/review link from Google's own "ask for reviews" button is already the best form there is.

  3. Is it a short link?

    A maps.app.goo.gl link — what the Maps share button gives you — is followed server-side to the full URL. Hard limits: two hops, five seconds, and each hop must still be a Google host. This is why the conversion endpoint requires a login: otherwise it would be an open URL fetcher for anyone.

  4. Pull out the feature id and rebuild.

    The owner is shown exactly what changed: "We converted your Maps link so it opens the review box straight away." Nothing is ever silently rewritten.

  5. A ?cid= link is saved but flagged.

    A CID is only half the feature id, so the review link cannot be built from it. Rather than quietly save something that lands on the listing, the owner is told to re-copy the URL from the browser bar while viewing the place.

Standing risk — put this in the quarterly diary The !12e1 format is undocumented. Google can change it without notice. If they do, every plaque already glued to a counter quietly starts landing on the listing page instead of the review box — no error, no alert, just a collapse in conversion that nobody would spot for weeks. Nothing in the system monitors this. Once a quarter: tap a real plaque, give five stars, confirm the review box actually opens.

Turning the paid part on — the exact steps

Everything above the line works with no key. Below the line is what a key buys, and it is one key for all four paid calls: GOOGLE_PLACES_KEY. It is not set in production today, which is why every venue card shows a lettered tile instead of a photo and the rating-over-time chart is permanently empty.

  1. Create a Google Cloud project

    Under the nudgetags.com Workspace account, not a personal one, so it survives a laptop change. Attach a billing account.

  2. Enable two APIs — they are separate switches

    Places API (New) and Street View Static API. Enabling one does not enable the other, and the failure mode is a silent null rather than an error.

  3. Set a budget alert and a quota cap before anything else

    Google removed the old flat $200/month credit in 2025 in favour of per-SKU free allowances. Nothing in our code caps spend. Set a hard daily quota per API and a billing alert at a figure you are willing to lose.

  4. Mint two keys, not one

    This matters. The shopfront photo is loaded by the customer's browser, which means the key ends up in the page source where any signed-in customer can read it. A server key must be IP-restricted or secret; a browser key must be referrer-restricted — and Google does not let one key carry both kinds of restriction.

    Key A (server): Places API (New) only, used by the lookup and the daily rating job. Key B (browser): Street View Static + Places Photo only, restricted by HTTP referrer to app.nudgetags.com. Splitting these is a small code change and it is the difference between a leaked key being harmless and a leaked key being someone else's bill.

  5. Set CRON_SECRET in the same deploy

    The daily rating job spends money per venue. The code fails closed — the moment a Places key exists without a cron secret, the job returns 503 rather than let an open endpoint run up a bill. Set both together or the ratings never start collecting.

  6. Add the key in Vercel and redeploy

    Project nudge-reviews → Settings → Environment Variables → Production. Environment changes need a redeploy to take effect.

  7. Backfill the venues

    The address lookup only fires when someone changes the Maps link in Settings — deliberately, so unrelated saves don't cost money. Existing venues will need their Maps link re-saved once each to pick up an address and photo.

"How is it enabled per venue?"

It isn't, and this is the single most misunderstood part of the setup. The key is global — one key, all customers. What varies per venue is whether that venue has given us something to look up. There are two different Google fields on two different screens, and filling one does not fill the other:

"Google review link" Free

Entered during onboarding, on the Reviews tab, or in the setup wizard. Stored in locations.google_url.

Job
Where a happy customer is sent. This is the revenue path.
Without it
The venue cannot collect reviews at all — the tab locks
Cost
Nothing, ever
"Google Maps link" Billed once

Entered in Settings only. Stored in locations.maps_url, then resolved to place_id, address, coordinates and a photo reference.

Job
Address and shopfront photo on the card, and it is what makes the venue eligible for the daily rating snapshot
Without it
Everything still works; the card is plainer and there is no rating history
Cost
One lookup when saved, then one cheap call a day forever

Only two link shapes are accepted for the Maps field: the address-bar URL of a venue's page (which always contains /maps/place/) and the short link the share button produces. A search URL is refused on purpose — a search can resolve to a different business with the same name, and the wrong shopfront on a customer's card is worse than no photo.

What a human has to keep an eye on

ItemHow oftenWhy
Tap a real plaque and confirm the review box opensQuarterlyThe deep-link format is undocumented and can change silently
Google Cloud billing and quotaMonthlyThe daily job is one billed call per venue per day — the bill scales with the customer count, and the free allowance changes
Key restrictions still correctOn any deploy that touches imagesA referrer-restricted key breaks server calls and vice versa
Rotate GOOGLE_PLACES_KEYOn any suspected leakUntil the key is split in two, the browser key is visible to every signed-in customer
Cached addresses and photosYearly, or when a card looks wrongNothing re-syncs them. A venue that moves keeps its old address forever, and photo references are not guaranteed stable
Apps Script deployment URLWhenever the script is editedRe-deploying can change the /exec URL, which silently breaks the finance dashboard until the Vercel variable is updated
Scaling ceiling worth knowing now The daily rating job works through venues one at a time inside a 60-second budget, with no pagination or resume. At roughly 300 ms a lookup that runs out somewhere around 150–200 venues, and the tail of the list would silently stop getting snapshots. It also has no retry and writes nothing to the error log, so a completely failed run is invisible unless someone opens the Vercel cron log. Fine at today's size; a real job to fix before the hundredth venue.

06 Every account and service in the stack

Ten external services. For each: what it does, where to log in, what it costs, and what happens if it disappears.

Vercel Live

Hosts all three websites. Builds them, serves them, issues the TLS certificates, and runs the two scheduled jobs.

Console
vercel.com → team purple-pi-marketing
Projects
nudge-reviews · nudgetags · nudge-tags-quote
Region
Functions pinned to syd1 — same continent as the database. This is not optional: when they were in Washington every click took 2–4 seconds and the product read as broken
Cost
Currently on a Hobby-tier footprint. The monthly-report job declares a 300-second budget, which exceeds the Hobby 60-second cap — confirm the plan before switching email on
If it dies
Every surface goes down. Plaques stop working
Note
The team is named after Gauresh's other business purely because a second Vercel team requires a payment method. Nothing customer-visible carries that name; the custom domains remove the last trace
Supabase (Postgres) Live

The database. Every account, venue, tap, rating and complaint lives here. Nothing else stores customer data.

Console
supabase.com → project nlpqsgqrixerlsqhmkms
Region
ap-southeast-2 (Sydney)
Connection
Through the transaction pooler on port 6543, with prepared statements disabled. Both are mandatory for serverless and must not be "cleaned up"
Plan
Pro — so no project-pausing risk
If it dies
The whole product stops, including every plaque. This is the single point of failure
Check
Whether the 13 tables show as "Unrestricted" in the table editor. Supabase exposes public tables over its own API by default, and the app does not use that path at all — see the risk list
Google Workspace Live

The nudgetags.com mailbox — contact@nudgetags.com. Also the identity that owns the Drive folder, the accounting sheet and the Apps Script.

Console
admin.google.com/u/5/ (Chrome profile index 5)
Uses
Contact address everywhere, contract, the enquiry-form target, the intended email sender
Cost
Per-seat monthly
If it dies
Enquiries stop arriving and the contract's contact address bounces
Google Cloud (Places) Not set up

Would provide venue addresses, shopfront photos and the daily public-rating snapshot. Nothing depends on it to earn money.

Console
console.cloud.google.com — no project created yet
Key
GOOGLE_PLACES_KEY, not set
Cost
Per call, billed at the tier of the most expensive field requested. The daily job uses the expensive tier by necessity
See
The Google section for the full setup and the two-key rule
Stripe Sandbox only

Would take the subscription payments. Built, wired, and inert — every price and webhook exists in test mode only.

Account
acct_1U3sp5IiFcUCzjal — Nudge Tags' own, not shared
Test objects
Product prod_V40vszmxLJBgwi; $149/mo and $1,188/yr prices; a webhook pointed at /api/stripe/webhook with six events
Blocked by
Live mode needs business verification and bank details. Until that is done, no money can be collected through the product — every invoice is manual
Before the first real charge
Stripe Tax must be configured with an AU registration; the code requests automatic tax and checkout will hard-fail without it
Also
Everything created in test mode has to be recreated in live mode, and the four Stripe variables set in Vercel
Resend (email) Not set up

Would send all four of the product's emails. Without it every send is logged to the console and skipped — nothing crashes, nothing arrives.

Key
RESEND_API_KEY, not set
What is dark
Welcome email · password resets · private-feedback alerts · the monthly report
Worst consequence
A venue owner locked out of their account cannot recover it themselves — it becomes a manual job every single time
Second worst
A complaint from an unhappy customer sits unread unless the owner happens to log in
Setup needs
A verified sending domain for nudgetags.com — SPF and DKIM records added at GoDaddy, then kept in place forever
Google Apps Script Live

A 30-line script that reads the accounting sheet and hands it back as data, and now also accepts a closed sale and writes it into the Income tab. It is the bridge between the books and the finance dashboard.

Where
script.google.com, on gaureshbusiness@gmail.com, project name literally "Untitled project"
Deployed as
Web app, execute as me, access "anyone" — the shared token is the only guard
Rotate
The token lives in two places that must change together: Script Properties → FEED_TOKEN, and Vercel → FINANCE_FEED_TOKEN
Trap
Editing the script does nothing until you Deploy → Manage deployments → New version. Adding a new tab to the sheet also requires editing the tab list in the script and redeploying
Trap
The reference copy in the repo has drifted from what is deployed — it has no write handler and does not list the Inventory tab. Do not rebuild from it without re-reading the live script
GoDaddy Live

Domain registrar and DNS for nudgetags.com. Every subdomain and the Workspace mail records are edited here.

Watch
Auto-renew and the card on file. A lapsed domain takes the site, the app, the plaques and the email at the same moment
Rule
Nameservers stay at GoDaddy. Moving them to Vercel would take the mail down
FormSubmit Unverified

A free relay that turns the marketing site's enquiry form into an email. No account, no key — which is also the problem.

Endpoint
formsubmit.co/ajax/contact@nudgetags.com, set on the last two lines of contact.html
Human step
The first submission triggers a one-time activation link sent to that mailbox. Until somebody clicks it nothing is delivered
The real danger
An un-activated or throttled form still returns a success status, so the visitor sees "Got it" while the enquiry evaporates. Test it end to end and confirm a real email lands
Replacing it
One line at the bottom of contact.html. Our own forms platform or a Resend-backed endpoint would remove the dependency entirely
Alibaba suppliers Live

Where the plaques are actually made. Two factories used so far, four orders, 505 units.

Leadercolor
Shenzhen Leadercolor Smart Tech — orders 1 & 2, 200 plain review cards
Huasentong
Shenzhen Huasentong Intelligent Technology — orders 3 & 4, custom rounded squares in black/white/sand and 5 green
Receipts
Drive › Nudge Tags › Accounting › Expenses, INVOICE ALIBABA 1–4.pdf
Watch
Shipping is 29% of everything spent. Freight is the lever, not unit price
Google Drive Live

Brand assets, print masters, spec sheets, the contract, the accounting sheet, the outreach tracker and this manual. Also the only backup of the source code.

Path
My Drive › BUSINESS › Nudge Tags
Rule
Drive is the source of truth for artwork, never the local copies — a stale local duplicate once shipped an old plaque design to the live website for hours
Access
Owned by gaureshbusiness@gmail.com. Leon's access should be checked folder by folder
Version control Does not exist

There is no git repository for any part of Nudge Tags. No history, no branches, no way to see what changed between two deploys, no way to revert.

Only copies
This Mac, and the Drive backup of the same folder
Consequence
A bad overwrite is unrecoverable. Two people cannot work on it at once
Fix
A private GitHub repo. An hour's work, and it is the highest-value maintenance item on this page

07 Keys and settings — the complete register

Every setting the app reads, whether it is set today, and exactly what changes if it is wrong. Set in Vercel → project nudge-reviews → Settings → Environment Variables → Production. Changes need a redeploy to take effect.

10
Set in production
4
Missing, and it matters
2
Deliberately unset
3
Credential-grade
SettingIn plain EnglishIf it is missing or wrongState
DATABASE_URL The connection to the Supabase database. Must be the pooler on port 6543. Everything stops. In production the app deliberately refuses to start rather than silently fall back to the throwaway local database, because that would look fine while quietly losing every rating. Set
AUTH_SECRET Signs the login cookie, and salts the one-way visitor hash. Missing: nobody can log in at all, though the marketing site and the plaque pages keep working — that exact symptom is the tell. Rotating it signs every user out everywhere (this is the only global sign-out we have) and re-buckets unique-visitor counts for that day, so rotate late at night. Set
APP_URL The real public address. Every QR code and every plaque link is built from it. Wrong value = printed plaques pointing at a dead address, and the NFC chips are locked after writing, so the fix is physically replacing tags on counters. The single most expensive setting to get wrong. Set
SUPERADMIN_EMAILS Comma-separated list of email addresses that get the operator console, the finance dashboard, the quote builder and the power to delete a venue. Empty or mistyped: no new customer accounts can be created and the operator link simply stops appearing, with no warning. Containing an address you don't control hands over every customer's details and the real revenue figures. Treat as a credential. Set
SUPPORT_EMAIL The contact address printed on the Terms, Privacy and Refund pages. Falls back to contact@nudgetags.com. Set
CRON_SECRET The password Vercel sends when it fires a scheduled job, so nobody else can trigger them. While both jobs are switched off it changes nothing. The moment a Google key or an email key exists, an unset secret makes the job return 503 and stop running entirely — a deliberate fail-closed so an open endpoint can never spend money. Set
FINANCE_FEED_URL
FINANCE_FEED_TOKEN
The Apps Script address the finance dashboard reads the books from, and the shared password on it. Either missing: the dashboard shows "Feed not configured". Mismatched token: "Feed refused". The URL plus the token together are read access to the entire company books, and the token travels in the query string, so it lands in logs. Rotate both halves together. Set
STRIPE_PRICE_MONTHLY
STRIPE_PRICE_YEARLY
The IDs of the $149/month and $1,188/year recurring prices in Stripe. Set, but useless without the secret key. They currently point at test-mode prices and will all need replacing with live-mode IDs. Set (test)
STRIPE_SECRET_KEY The key that lets the app talk to Stripe at all. The billing page shows "Card payments aren't switched on in this deployment yet" and two price cards with no buttons. No customer can pay through the product. Nothing else breaks — accounts stay fully live. Missing
STRIPE_WEBHOOK_SECRET Proves an incoming payment notification really came from Stripe. Every webhook is rejected, which means plan status can never change automatically — no account can ever become "active", and a cancellation would never take effect. Missing
RESEND_API_KEY Lets the app send email. All four emails silently do not arrive: welcome, password reset, private-feedback alerts, monthly report. Nothing crashes; the UI is honest about it in three places. The practical cost is that every locked-out customer is a manual job. Missing
MAIL_FROM The From address on those emails. Falls back to Nudge Tags <contact@nudgetags.com>, which is correct — but that domain must be verified in Resend before anything will send. Default
GOOGLE_PLACES_KEY One key for all four paid Google Maps calls. No venue addresses, no shopfront photos, and the "your Google rating over time" chart stays permanently empty — which is the chart that proves the product worked. Nothing that earns revenue is affected. Missing
SIGNUP_OPEN Set to 1 to reopen public self-service sign-up. Unset means closed, which is intended — the product is sold in person and an account should exist because someone bought it. Leaving it open once handed a fully working venue to anyone who guessed the URL. Unset on purpose
DATABASE_POOL_MAX How many database connections each server instance may hold. Defaults to 3. Nothing — leave it alone unless connections are being exhausted. Unset on purpose
Three things that are credential-grade and don't look it 1. SUPERADMIN_EMAILS — an email address in a list is the only thing standing between someone and the real revenue figures. 2. The finance feed URL + token — anyone holding both can read the entire books, and the token sits in a query string. 3. .env.production.local in the app folder holds the live database password in plain text; the folder it sits in is backed up to Google Drive. Its own header says "keep it off Drive" — worth confirming that is actually true.
The order to switch things on 1. CRON_SECRET is already set, so the jobs are safe to enable. 2. Resend — a verified domain plus the key — turns on password recovery, feedback alerts and the monthly report in one move; this is the highest-value key on the list. 3. The Google Places key, split into two restricted keys, turns on the proof-of-value chart. 4. Stripe last, because it needs business verification first and everything works without it.

08 What we store, and where

Twenty-one tables in one Postgres database in Sydney. This is every piece of data the business holds about a customer or their customers.

TableOne row is…Written whenWorth knowing
businessesOne paying account — a café owner, a pub groupYou create it in /adminHolds the plan status, the venue limit and the Stripe ids. billing_exempt marks our own accounts as never-charged
usersA person who can log inAccount creation, or a staff member accepting an inviteEmail is unique globally — one address can only ever belong to one account, forever. Passwords are PBKDF2 at 210,000 iterations
locationsA venue. The most important tableOnboarding, or adding a venueCarries the public slug, the Google link, the menu link, the star threshold, the timezone, the cached Google address, and the on/off switches. 24 columns are editable and the update is restricted to an explicit allow-list so a crafted form can't write anything else
tagsOne physical placement — a plaque, a table, a staff card, a receipt footerAdding a placement, a table range, or a staff memberIts code is what appears in the link as ?c=. A staff card is a tag with kind "staff". Its code is random and carries no name, because it is written to the chip and has to outlive whoever holds it — the person lives in staff_members
visitsOne page openEvery tapThe fastest-growing table. Stores the day in the venue's own timezone, the device type, and a salted hash of IP + browser + date. The IP is never stored and the hash resets nightly
ratingsOne star tapEvery ratingRecords the stars, which placement they came from, and where they went: google, private, or staff — a staff rating rates the person and never reaches Google
feedbackA private complaintA gated customer submits the formThe optional contact field is the only personally identifying data in the system. The CSV export defuses spreadsheet formula injection, because this text comes from strangers
place_snapshotsA venue's public Google rating on one dayThe daily job, once a key existsStored as daily snapshots rather than one current value, so the dashboard can draw the line that sells the product
invitesAn unused team-invite linkOwner generates oneOnly the hash is stored, so a stolen database can't be replayed as working links. 14-day expiry, single use, revocable instantly
user_locations"This member may see this venue"Scoped invite, or editing accessSee the risk list — the edit form has a bug here
password_resetsAn outstanding reset or setup linkForgot-password, or creating an accountHashed, single use, race-safe
rate_limitsA flood counterConstantlyKept in the database rather than memory because serverless instances don't share memory
error_logA crashAny uncaught error, server or browserWhat the operator console shows. Contains customer email addresses — internal eyes only

What happens when you delete something

Deleting a business erases that customer completely: their users, venues, tags, every tap, every rating, every complaint and every pending invite. Deleting a venue erases all of that venue's history. There is no soft delete, no archive and no undo. This is why deleting a venue is the most locked-down action in the whole product — it needs a superadmin email, the owner role, typing the venue's exact name, and re-entering the account password.

The intended way to switch something off is the active flag, not deletion. Pausing keeps the printed QR codes and plaques valid so they work again the moment you switch back on.

Changing the database

  1. Nothing runs automatically.

    Not on deploy, not on start-up. A release that needs a new column will crash in production until a human runs the migration by hand. This is the most important operational fact in this section.

  2. The command
    cd ~/Documents/claude/nudge-tags/nudge-reviews
    DATABASE_URL="<the pooler string>" npm run db:migrate
  3. There is no ledger of what has run.

    Every migration file is re-executed every time. It is safe only because each one is written to be repeatable — but see the risk list, one of them is not, and it quietly re-grants access on every run.

Data retention — the honest position Only two things are ever cleaned up automatically, and even that happens by chance rather than on a schedule: expired flood counters and used reset links. Taps, ratings and complaints grow forever, including the customer contact details someone left on a private feedback form. The privacy pages promise retention windows — 12 months for tap records and feedback, 90 days after account closure — and nothing in the database currently enforces any of them. That needs either a scheduled clean-up job or an honest rewrite of the policy.

09 Every form, and where it ends up

One rule: nothing is captured that isn't stored somewhere we own and shown on a screen someone opens. If a row can't be pointed at on this page, it shouldn't be collected. Audited end to end on 22 August 2026.

Customer-facing — the things a member of the public touches

Capture pointTriggered byStored inShown on
/r/<slug>?c=<code>Any plaque, table or staff card tapvisitsOverview, Google Reviews, Menus, Staff — every tap chart
/r/<slug>/rateThe star tapratingsStar funnel, placement board, staff board, monthly report
/r/<slug>/feedbackA private message after a ratingfeedbackFeedback tab, CSV export, owner email alert
/m/<slug>A menu table tapvisitsMenus tab
/api/tap-locationStaff card tap, integrity on, customer agreestap_locationsConsumed into an integrity flag, then deleted on a retention timer
/api/enquiryThe contact form on nudgetags.comenquiriesOperator dashboard → Website enquiries, plus an email
/w/<key>The public review widget on a venue's sitenothingNothing — see the gap below

Dashboard — the things a customer or an operator submits

Capture pointStored inShown on
Add venue / edit venue / setuplocationsLocations, Settings, Setup
Add placement, table range, staff cardtagsLocations, Menus, Staff
Add or reassign a staff memberstaff_members, staff_assignmentsStaff board and Past team members
Set the Google review linklocations, review_link_logLocations, with the paste-vs-converted history
Roster and integrity reviewrosters, integrity_flagsStaff tab integrity panel
Generate a reportreportsReports tab, and the PDF at /dashboard/reports/<id>
Team invite / acceptinvites, users, user_locationsSettings → Team
Signup, login, password resetusers, password_resetsOperator dashboard → All accounts
Client-side crasherror_logOperator dashboard → Recent errors
Stripe webhookbusinessesBilling, Operator dashboard, /finances
Quote builder → log a closeGoogle Sheet via Apps Script/finances, and the accounting sheet

Why a new link is covered automatically

New tables, staff cards and placements need no wiring. A tap records the ?c= code onto the visit, and every board reads whatever tags that venue currently has and joins on that code. Nothing enumerates placements in code, so a placement created a minute ago appears on the next page load with no deploy and no configuration.

The one thing that escaped this was traffic on a link with no matching placement — a bare /r/<slug> records as direct, and a QR printed before a tag existed keeps its old code. Those taps counted in the venue's totals but appeared in no row, so the total quietly exceeded the sum of its parts. The placement board now ends with a “Not on a placement” line, so the two always reconcile on screen.

Known gaps

GapConsequenceStatus
Widget views at /w/<key> are not recordedA venue can embed our widget and show it thousands of times with nothing counted — no reach figure, and no way to prove the widget is worth keepingOpen
The enquiry endpoint accepts requests with no Origin headerA script can post junk leads. Rate limit is 5 per 10 minutes per IP and the honeypot catches naive bots; origin checks are not a real control against a non-browser clientAccepted

Fixed in this audit

  • The website contact form posted to formsubmit.co, a third-party relay that emailed the inbox and stored nothing. No count, no list, no way to tell a quiet week from a broken form — and if the relay was never activated, early enquiries were lost silently. It now posts to /api/enquiry: stored first, emailed second, listed on the operator dashboard.
  • Customer GPS fixes were kept forever. location_retention_days existed as a setting with no code behind it. The daily job now deletes fixes past their venue's window, defaulting to 30 days where none is set.
  • Staff ratings were counted as Google review activity. They now record their own surface and never route to Google.

10 What runs on its own

Two scheduled jobs, one polling loop, and one thing that cleans up by chance. That is the entire list of automation — everything else only happens because a person clicked something.

JobWhen (Brisbane)What it doesRunning?
Daily rating snapshot
/api/cron/places
05:00 every day
(19:00 UTC the day before — Vercel schedules are always in UTC)
Asks Google for the current public star rating and review count of every venue that has a resolved listing, and files one row per venue per day. A failed lookup is skipped rather than written as a zero — a gap in the line is honest, a false 0.0 would look like every customer turned on them. Skips
no Google key
Monthly report
/api/cron/report
07:30 on the 2nd
(21:30 UTC on the 1st)
Emails every account owner a per-venue summary: ratings collected, how many went to Google, average stars, how the public rating moved, and the top three themes from their private feedback. Cancelled accounts and venues with no activity are skipped on purpose — a "you got 0" email is how a report becomes the thing that reminds someone to cancel. Skips
no email key
Finance dashboard poll Every 60 seconds, while the page is open Re-reads the accounting sheet so an edit made on a phone appears on the dashboard within a minute. Pauses when the tab is hidden. Live
Housekeeping Roughly 1 time in 100, whenever someone taps a star Deletes flood counters older than a day and expired reset links. Not a schedule — a coin flip. If the product goes quiet, cleaning stops happening. Live
Both jobs are written to fail closed Each one checks the cron password first. If the password is not set and the feature it drives is switched off, it does nothing and says so. If the password is not set but the feature is on — meaning the job would now spend real money — it refuses to run at all rather than leave an open endpoint that anyone could trigger to run up a bill. That is why the cron secret must be set in the same deploy as the key it protects, never after.
Known weakness in the monthly report It works through every business one at a time with no record of who has already been emailed. If it times out halfway, some owners have their report and some don't, nothing records which, and re-running double-sends to the first group. Harmless at four customers; a real problem at forty. Worth fixing before the first month it actually sends.

11 The money

What we charge, what it costs us, where it is recorded, and the places the numbers currently disagree with each other.

The model in one line

Hardware is the wedge; the subscription is the business. Plaques sell from a published price list plus a flat install — deliberately low friction, because the point of the plaque is to get the subscription signed. Prices are no longer derived from landed cost, so a supplier price change moves our margin rather than silently moving the customer's quote.

Plaque price list (AUD, per unit)1–910+LandedMargin at 10+
Regular review cards$15$10$2.4475.6%
Custom review cards — black, white, sand, green$20$15$3.19 (green $5.85)78.7% (green 61.0%)
Installation$50 flat, once per quote — never per plaquepure margin
Two things about this list that matter on a call The band is judged on the family total, not each colour. Five black plus five white is a ten-plaque order of custom cards, so both lines get $15 — the four colours are stock of one product, not four products.

There is a cliff, and it is worth using. Ten regular cards cost $100, so anyone buying 7, 8 or 9 is paying more than a ten-pack — nine costs $135 for one plaque fewer. Same for custom at 8 or 9 against a $150 ten-pack. The quote builder now says this out loud when it happens: "1 more and every one drops to $10."
Plaques
Sell
From the price list above — set prices, not a cost multiple
Install
$50 flat, once per job — not per unit
Bands
Decided by how many of that family are on the quote; colours count together
Margin
Never below 61% on any line at any band, before the install fee is counted
Software — the MRR
Price
$149 per venue per month, or $99/month billed yearly ($1,188)
Unit
Per venue, not per plaque and not per feature. Reviews, menus or both — same price
Team
Free. Only the account owner is billed, however many people have access
Rule
Every hardware sale includes it. Non-negotiable
Where the figures live
Software price
One constant in the code. Changing it changes the marketing page, the billing page and the legally binding Terms at the same moment
Plaque price
Calculated per deal in the quote tools; never published
Rule
No plaque price on any public page. Price is disclosed in person or in the contract

What has actually been spent, and on what

Four supplier orders, 505 units, A$1,475.66 — a blended landed cost of $2.92 a unit. Revenue to date is $0, so the current net position is −$1,475.66.

OrderDateSupplierUnitsTotal AUDLanded / unit
1 — plain review cards10 AugLeadercolor100$201.79$2.0179
2 — plain review cards10 AugLeadercolor100$286.52$2.8652
3 — custom squares, 100 each black/white/sand16 AugHuasentong300$958.08$3.1936
4 — custom squares, green18 AugHuasentong5$29.27$5.8540
Total505$1,475.66$2.9221

Shipping is 29% of everything spent. Freight, not unit price, is the lever worth negotiating. Orders 1 and 2 are still USD estimates converted at 1.43117 — the sheet's own yellow cells flag them. Until they are replaced with the actual AUD charged, every cost, margin and breakeven figure carries a small FX error.

The accounting chain

  1. One Google Sheet is the book of record.

    "Nudge Tags Accounting", in Drive › Nudge Tags › Accounting. Three tabs: Expenses (one row per supplier order, split into goods / shipping / tax / fees), Income (one row per sale), Inventory (each product's landed cost per unit and how many were bought). Receipts sit alongside it as PDFs.

  2. A small Apps Script hands the sheet over as data.

    Guarded by a shared token. It is also what receives a closed sale from the quote builder and writes it into the Income tab.

  3. The dashboard reads it live.

    app.nudgetags.com/finances, superadmin only, refreshing every 60 seconds. Five sub-tabs — Overview, Revenue, Expenses, Units & Inventory, Ledgers — and five time ranges recomputed in the browser, so switching range never disagrees with the server.

  4. Nothing is ever written back except a closed sale.

    If a number is wrong on screen, the fix is always in the sheet.

The rule that keeps the books honest The code counts columns, it does not read headings. Insert or delete a column in the Expenses, Income or Inventory tab and the dashboard will keep working, keep looking correct, and be completely wrong. Two more traps of the same kind: the Category cell must read exactly Inventory (not "inventory", not "Stock") or unit economics silently collapse to zero and margin reads 100%; and software revenue must use a Type containing "subscription", "software" or "saas" with Qty 0, or it books as hardware and skews every per-unit figure. Data-validation dropdowns on both columns would remove the whole class of error.

Two quote tools — and which to use

quote.nudgetags.com Retired & secured

The original standalone calculator. One HTML file, works offline, no login. Quantity, discount and landed cost in; sell price, total, cost, gross profit, margin and a margin-vs-quantity chart out.

Fixed
20 Aug 2026 — the domain and the old project URL both redirect to the login-gated builder. The source file is kept locally and in Drive for reference only
Also
The subscription line is stubbed off, so it quotes hardware only — which contradicts the rule that every sale includes the software
Also
Install is costed at $0, so margin is overstated by whatever an install actually costs in travel and labour
app.nudgetags.com/quote Behind login

The newer in-app builder. Prices against real stock and real landed costs pulled live from the Inventory tab, shows the margin panel only to us, and logs the closed sale straight into the Income tab — one row per product line, matched by product name so stock-on-hand follows automatically.

Pricing
Nothing is typed in. Each product is priced at 3× its own landed cost — rounded up to the nearest 50c, $5 floor — so a $2.44 review card quotes at $7.50 while a $5.85 green custom quotes at $18.00. Pick quantities; every figure follows.
Mixed orders
One quote line and one margin row per variant, so a healthy blended margin can't hide one product being sold at almost nothing. Discount applies across the plaques; install is a flat $50 once per quote
Gate
Superadmin only
Safety
The browser never sees the sheet's write token, and a reference on each sale makes a double-click harmless
Verdict
Use this one. Then either lock the public calculator down or retire it

Where the documents disagree about money

These are live contradictions between things a customer could be handed. They are ranked by how much trouble they could cause.

#The disagreementWhy it matters
1GST. The old pitch PDF says prices include GST · the contract says exclusive of GST · the website terms say software pricing includes GST · the quote tool correctly says we are not GST-registered at allA business that is not registered cannot charge GST. Two customer-facing documents are wrong, and one of them is sitting in Drive › Sales
2Lock-in. The pitch says "no lock-in, cancel with 30 days' notice, no exit fee". The contract has a 6- or 12-month minimum with a 50%/30% early-termination fee. The home page says "no lock-in" while its own FAQ describes the minimum termHanding a venue the pitch and then the contract is a misleading-conduct exposure, not just untidy copy
3Who owns the plaque. Pitch: "keep the plaques". Contract: Nudge-branded plaques come back within 14 days or are invoiced. The app's own Terms: "physical plaques are yours to keep"Three documents, three answers, one of them inside the product a customer logs into daily
4What we charge per. Pitch prices per venue tier · contract prices per plaque per month · the software prices per venue. The volume ladder the website promises is written down nowhereYou cannot fill in the contract's fee schedule from any existing price list
5Liability cap. Contract says 12 months of fees; the app's Terms say 3 monthsA customer who buys both has two contracts and no clause saying which wins
6The contract never mentions the software at allThe subscription is the main revenue line and it is not in the agreement that gets signed

12 The physical product

Exact specification, the colour codes, and the pipeline that turns a Python script into something a factory can quote.

Round — the general plaqueSquare — the review plaque
JobPoints at anything: menu, ordering, bookings, wine list, an event, Instagram. No stars, no mention of reviews.Google reviews only. Five brass stars and "LEAVE US A REVIEW" so a customer knows what it is across the room
SizeØ 90.0 mm100 × 100 mm, measured to sharp corners before the R16 radius is taken off
Thickness3.0 mm cast acrylic, polished edge, R0.5 edge break
ChipNTAG215 or better — the general plaque does more workNTAG213
Chip fittingØ 25 mm coin tag, surface-stuck on the back, centred. Reads through the 3 mm face — no recess needed
Adhesive3M VHB, Ø 86 mm, full base, keep-out over the chip3M VHB, 96 × 96 mm, full base, keep-out over the chip
FinishesSand #E7D9BC (default) · Ink #0B0B0D · Chalk #FFFFFF · Forest #43792C, all with brass #C2A15C accents
Three rules baked into the artwork Never metal — standard NFC will not read through it, which is why these are acrylic and always will be. No QR codes, anywhere — not printed, not optional, not as a fallback; the plaques are tap-only. The review plaque speaks in the venue's voice, never ours — it says "leave us a review" with a small "powered by nudge." sign-off, because a customer is being asked by the café, not by a supplier they have never heard of.

How a plaque gets made

  1. Draw it

    Python scripts in nudge-tags/brand/ emit the artwork as SVG with all type converted to outlines, so the files open correctly anywhere without the font. make_plaques_all.py for round, make_square_reviews.py for square.

  2. Export to print resolution

    sips -s format png -Z 2126. Never qlmanage — Quick Look pads and crops non-square artwork into a square thumbnail and silently wrecks the drawing. The round master is 600 dpi at Ø90 mm; the square is 540 dpi at 100 mm.

  3. Copy into Drive

    Renders › "Circles — General (not reviews)" and "Squares — Review Plaques". Drive is the source of truth, not the repo — a stale local duplicate once shipped an old square design to the live website for hours before anyone noticed.

  4. Regenerate the spec sheet

    A landscape technical drawing with top, side and back views, every dimension, the adhesive area, the chip keep-out and five manufacturing notes. This is the PDF that goes to a factory with an RFQ.

  5. Rebuild the website textures

    python3 website/build_assets.py reads the print PNGs from Drive and flattens them into the textures the 3D showroom uses — then bump ART_VERSION in two files by hand or browsers keep serving the old print for up to a year.

  6. Order, write, lock, install

    Factory produces; each chip is written with the NFC Tools app and locked; plaque goes on with 3M VHB.

Four problems in this pipeline right now 1. The circle spec sheet states the wrong chip. It says NTAG213; the round plaque is meant to be NTAG215 or better. The website already says it correctly — but the spec sheet is the document that goes to suppliers. Fix and regenerate.
2. The build guide in Drive carries Purple Pi branding — a byline and a purplepimarketing.com link, inside a 50/50 business. It also still instructs printing a QR code on every plaque, which is banned. Both make it stale and wrong; rewrite or delete it.
3. Running either spec generator silently rewrites all eight plaque masters, including four retired designs, because of an import side effect. Check what changed before copying anything to Drive.
4. Nobody but Gauresh can regenerate artwork. The pipeline needs macOS, a specific system font, sips, and the Drive mount under his account. Leon cannot produce a spec sheet today.

Chips and suppliers

What the chip is

A passive NTAG NFC sticker. No battery, no radio of its own, no certification required — the phone powers it for the instant it is read. This is why there is nothing to charge, nothing to fail and nothing to approve.

Sourcing
Now
Complete plaques from Alibaba (Leadercolor, Huasentong) at $2–3 landed
Local fallback
Core Electronics, Newcastle NSW — Ø25 mm NTAG213 coin tags around $1.45 each including GST, MOQ 1, in stock
Benchmark
ReviewBoost Australia sells one plaque at $69, five at $199
Ignore this file

nudgetags-chip-supplier-research.html is 47 KB of research into ESP32 modules — a powered IoT device this product is not. It has factory lead times, reel minimum-order quantities and compliance requirements that are all irrelevant. Delete it before anyone hands it to a supplier.

13 The commercial layer

The partnership, the contract, how we sell, and who we are selling against.

The partnership, as it stands on paper

What exists Registered
ABN
64 539 870 400 — used consistently in the contract header, all eight website footers and the product's legal pages, all fed from one place in the code
Minutes
"Minutes of Meeting Nudge Tags", 16 Aug 2026, DocuSigned by both founders. Records Gauresh Malhotra 50% and Leon Allendes 50%, both as beneficial owners — the declaration banks and AUSTRAC ask for
What does not exist Open
Company
This is a general partnership, not a Pty Ltd. Both founders are personally liable — while physically installing objects in commercial venues
Business name
"Nudge Tags" is not registered with ASIC. A partnership trading under a non-surname name is required to register it (~$45/yr)
GST
Not registered. Correct below the $75k threshold, but three documents currently say otherwise
Trademark
No filing, despite the contract and the website both asserting ownership of the name and marks
Partnership agreement
The minutes record percentages only. Nothing on profit distribution, drawings, deadlock, IP, buyout, or what happens if one of you can't continue
Insurance
No public liability or professional indemnity evidence on file
Fix before the next signature

The DocuSign certificate on the founding minutes records Gauresh's signer title as "CEO, J AND G OPERATIONS PTY LTD" — the wound-down previous business — and the account is still set to US Pacific time. Both are profile settings. Correct them before any customer contract goes out through the same account.

The customer Service Agreement

Seventeen numbered clauses. The canonical copy is in Drive › Nudge Tags › Sales; it is generated by a script, so the way to change it is to edit the generator and re-render, never to hand-edit the document.

ClauseWhat it commits us to
2 — ServicesSupply and physical install; each plaque programmed to a unique managed link we control; destination changes free, actioned within 2 business days; a monthly report of taps per plaque and the change in Google review count; free replacement of plaques that fail in normal use
3 — The link systemOn termination the plaques stop directing anyone anywhere and can no longer be updated. This is the leverage, written down
4 — FeesAll AUD, exclusive of GST. Setup fee payable on signing before installation and non-refundable once produced. Monthly fee per plaque, rate set by total count and by branding choice. Billed monthly in advance, due within 7 days. Every figure is a blank filled in per deal. Service may be suspended if more than 10 days overdue, after notice
4.4 — Price changesInside the minimum term, only by mutual written agreement. After it, we may revise on 30 days' notice and the customer may leave fee-free before it applies
5 — ComplianceWe state that we do not write, buy, incentivise, filter or gate reviews, and do not screen customers to send unhappy ones elsewhere
6 — OwnershipNudge-branded plaques remain our property and come back within 14 days of termination, or are invoiced at replacement cost. Custom-branded plaques become the customer's once paid
7 — Term6 or 12 month minimum (tick one), then month-to-month. 30 days' notice by text or email. Early termination: 6-month term = 50% of remaining fees; 12-month = 50%, dropping to 30% after month six. Framed as a genuine pre-estimate of costs, which is what makes it enforceable under Australian Consumer Law
12 — PrivacyTap data is date, time, which plaque and broad device type. No names, phones, emails or location. "Reports contain counts only"
13 — LiabilityNo guarantee of reviews, rating or ranking; not liable for Google; capped at fees paid in that 12-month period; ACL rights preserved
16 — LawQueensland. Good-faith discussion, then mediation with costs shared, then the courts
The clause that needs a decision, not an edit Clause 5 says we do not screen customers to send unhappy ones elsewhere. The product ships a configurable star threshold that does exactly that when a venue sets it above 1. The default is 1 — everyone goes to Google — and the marketing site addresses the policy openly, so the position is currently honest. But it is a setting, not a guarantee, and the contract makes it a promise. Either the threshold becomes fixed at 1, or clause 5 has to describe what the product actually does. Two other gaps in the same document: it never mentions the software at all, and clause 12's description of what we store is narrower than what the dashboard actually holds now that private feedback and optional contact details exist.

How we sell

The outreach sequence

Post-visit, three emails: day 0, day 5, day 12. Stop on a reply, stop on a no.

Day 0
A real detail from the visit in the first two lines · their review count against the size of the room ("not a service problem, an asking problem") · one line on the plaque · what the software does · the three tiers named but never priced · one call to action: ten minutes in person
Day 5
Pre-answers the objection — "we already ask people" — and closes on one question: how many did you actually ask last month?
Day 12
Closing your file. Graceful, with a verifiable competitor comparison, door left open
The rules
  • One real visit detail, always. Never invent one
  • Look up the review count first — the number does the arguing
  • Under 250 words, one call to action
  • No price in writing. Deflect to in person
  • Australian spelling, no emojis, no exclamation marks
  • Never claim we block, hide or delete a bad review
  • Never claim a specific lift — we do not have that data yet
  • Never use the Purple Pi address
Two trackers, and that is one too many

The Outreach Tracker (16 columns, dropdowns, a formula dashboard) is what the script tells you to log into — and it is empty apart from its example row. The real leads live in a different Drive file, "Brisbane_NFC_100_Prospects", with 100 named venues, phones, priorities and a suggested door-knock order across eleven Brisbane areas — and different columns.

Do
Pick one before either accumulates history

The competitor: Quick Suite

Around 200 clients across AU, US, CA and NZ, month-to-month with no contracts, built solo by its owner. Broader software than ours — review replies, SEO blogs, social posts and a chat assistant, all branded as named AI characters — and a genuinely strong proof asset in a live client dashboard showing a café going from 28 to 674 reviews. Their hardware is included in the subscription, generic, and the client keeps it on cancellation. They do not do NFC menus.

The honest read from our own dossier: their hardware is worse than ours and their software is deeper than ours. Their exposed flank is compliance — they are actively expanding a "not happy? tell us" route and they generate review text for customers to paste into Google, both of which breach Google's policy and endanger the client's listing, not just theirs. That attack line only works while our own thresholds stay at 1, which is why the threshold question above is a positioning decision rather than a settings detail.

Two notes for the record: the intel came from a call where Leon posed as a buyer for his father's restaurant; and their claim that "47% of people in Queensland use AI to find local businesses" is unverified and probably invented — do not repeat it. Their actual pricing is still unknown, and getting hold of their two emailed proposals remains the single highest-value open item for setting our own price.

14 Playbooks

The dozen things that actually get done, written so either of you can do them without asking the other.

Onboard a new venue, from signed deal to live plaque

  1. Sign the agreement

    Fill in the customer block and the fee schedule in clause 4.2 — branding choice, plaque count, setup fee, rate per plaque, monthly total, start date. Tick the 6 or 12 month term. Both founders sign as Co-Founder. Send as PDF, not .docx: the brand font is not embedded.

  2. Create their account

    app.nudgetags.com/admin → business name, owner's name, owner's email. You get a setup link that is valid 7 days and shown exactly once. Copy it before leaving the page — there is no resend button, and with email switched off there is no self-service recovery either.

  3. Send them the link

    They set their own password and land on onboarding: venue name, Google review link, threshold. Leave the threshold at 1 — everyone goes to Google.

  4. Set the venue up properly

    Paste the Google Maps link in Settings (this is the one that costs a lookup and gives you the address, photo and rating tracking). Set the venue's timezone if it isn't Brisbane. Add the menu link if they're running menus.

  5. Create a placement per physical object

    Reviews tab → Placements. One per plaque, one per table, one per staff member. Each gets its own ?c= code, and that code is the only reason "which table actually gets used" is answerable.

  6. Write and lock the chips

    NFC Tools app, one URL per plaque, matching that placement's link exactly. Lock every chip before it leaves your hands — an unlocked tag can be rewritten by any customer with a phone, and a locked wrong URL means physically replacing the plaque.

  7. Install and test on site

    3M VHB, never on metal. Then tap each plaque with a real phone and confirm it opens the right page — not the right venue, the right placement.

  8. Log the sale

    app.nudgetags.com/quote → build the order → log the close. It writes one Income row per product line and stock-on-hand follows automatically.

  9. Invoice

    Manually, for now — Stripe cannot take a card yet. Set a reminder for the recurring charge, because nothing in the system will do it for you.

Ship a change

The product
cd ~/Documents/claude/nudge-tags/nudge-reviews
npm run typecheck
vercel deploy --prod --yes \
  --scope purple-pi-marketing

Then open a review page and the dashboard and click through. There are no automated tests.

The marketing site
cd ~/Documents/claude/nudge-tags/website
vercel --prod

If you touched CSS or JS, bump the ?v= number on the link and script tags in all eight pages, or returning visitors keep the old one.

The quote calculator
cd ~/Documents/claude/nudge-tags/quote
vercel --prod

And copy the same file over the Drive copy, or the two drift apart.

Before every deploy There is no version control, so there is no undo. If you are about to change something you'd struggle to retype, copy the file somewhere first. And never overwrite an image under /assets/img/ — it is cached in visitors' browsers for a year and marked immutable; save it under a new filename instead.

Change the database

  1. Write a new numbered migration file

    In db/migrations/. Use ADD COLUMN IF NOT EXISTS / CREATE TABLE IF NOT EXISTS — every file is re-run on every migration, so it has to be safe to repeat. Never put a bare UPDATE in one; one existing migration does that and it silently re-grants access every time it runs.

  2. Update db/schema.sql too

    It is what a fresh database is built from. It is currently missing two tables that live only in migrations — don't add a third.

  3. Run it against production, by hand
    cd ~/Documents/claude/nudge-tags/nudge-reviews
    DATABASE_URL="<pooler string>" npm run db:migrate

    Nothing runs this automatically. Deploy the code only after the migration has run, or the site crashes for everyone in between.

Rotate a key

KeyHowBlast radius
AUTH_SECRETNew random value in Vercel, redeployEveryone is signed out everywhere, including you. Passwords still work. Unique-visitor counts wobble for that day, so do it late at night
FINANCE_FEED_TOKENChange it in two places together: Apps Script → Project Settings → Script Properties, and Vercel. Then redeployFinance dashboard says "Feed refused" until both match
DATABASE_URLRotate the password in Supabase, update Vercel, update .env.production.local, redeployEverything is down between the rotation and the redeploy. Do it deliberately
GOOGLE_PLACES_KEYNew key in Google Cloud with the same restrictions, swap in Vercel, delete the old onePhotos and rating snapshots pause until the redeploy
SUPERADMIN_EMAILSEdit in Vercel, redeployGrants or removes the operator console, the finance dashboard and venue deletion — all at once

Switch email on — the highest-value hour available

  1. Create a Resend account and add nudgetags.com as a sending domain.
  2. Add the SPF and DKIM records at GoDaddy

    Alongside the existing Google Workspace records, not instead of them. Wait for Resend to show the domain verified.

  3. Set RESEND_API_KEY in Vercel and redeploy.

    CRON_SECRET is already set, so the monthly report will not be blocked.

  4. Test all four

    A password reset to yourself; leave a 1-star rating on a test venue and check the alert lands; run the report job manually; create a test account and check the welcome email.

  5. Then tell customers about it

    Password self-service, feedback alerts and the monthly report all start working at once — and the monthly report is the single best anti-churn asset in the product.

Take the first real payment

  1. Activate the Stripe account

    Business verification and bank details. Gauresh only — never hand these to anyone else, and never enter them on someone's behalf.

  2. Configure Stripe Tax with an AU registration

    The code requests automatic tax on every checkout. Without this configured, creating a checkout session fails outright.

  3. Recreate the product and both prices in live mode

    $149/month and $1,188/year, AUD, recurring, priced per unit so the quantity equals the venue count.

  4. Recreate the webhook in live mode

    Pointed at https://app.nudgetags.com/api/stripe/webhook, subscribed to the six subscription and invoice events.

  5. Set four variables in Vercel and redeploy

    The secret key, both live price IDs, and the webhook signing secret. Until all four are set the billing page correctly says card payments aren't switched on.

  6. Mark your own accounts exempt

    Founder accounts are flagged by a database column and there is no admin screen for it — it needs a one-line SQL update against Supabase. Do this before testing, or you will invoice yourselves.

Monthly finance close

  1. Enter every supplier order

    One row per order in the Expenses tab, above the blank row so the sheet's own totals still cover it. Split the cost into goods, shipping, tax and fees — the grey cells are formulas, don't type over them. Category must read exactly Inventory.

  2. Replace any FX estimates

    Yellow cells are USD converted at an assumed rate. Replace them with the actual AUD your statement was charged. Orders 1 and 2 are still outstanding.

  3. File the receipt

    Drive › Nudge Tags › Accounting › Expenses, named to match the order.

  4. Check every sale is on the Income tab

    Sales closed through the quote builder land automatically. Anything invoiced by hand needs a row — with Qty 0 for software-only revenue and a Type containing "subscription", or the unit economics skew.

  5. Add any new product to the Inventory tab

    Product, type, description, supplier, landed cost per unit, units bought. The product name has to match what the quote builder sells, because that name is what links stock to sales.

  6. Read the dashboard

    /finances → Overview for net position and cash recovered, Units & Inventory for stock on hand and the stock-out horizon, Expenses to see whether freight is still 29% of spend.

Something is broken — triage in order

  1. Can you load a plaque link?

    app.nudgetags.com/r/<a real slug>. If yes, customers are fine and this is not an emergency.

  2. Check the operator console

    /admin shows the last 25 crashes with the path and the affected user, plus a 24-hour count. Start here before guessing.

  3. Marketing site fine but nobody can log in?

    That is the specific signature of a broken AUTH_SECRET.

  4. Everything slow?

    Check Supabase is up and that the Vercel functions are still pinned to syd1. Cross-Pacific queries once made the whole product feel broken.

  5. Dashboard fine, finance page dead?

    The Apps Script deployment URL or the token has changed. Both halves of the token have to match.

  6. Check Vercel's own logs

    Deployment logs for build failures, function logs for runtime errors, cron logs to see whether the scheduled jobs actually ran.

Give Leon access

Today he has none of the operator surfaces. To change that: he needs a user account in the product, and his email added to SUPERADMIN_EMAILS in Vercel, followed by a redeploy. Be deliberate — that one variable grants the operator console over every customer account, the real revenue figures and the ability to permanently delete a venue. There is currently no way to grant the finance dashboard alone; if that separation matters, it has to be built. He will also need: the Vercel team, the Supabase project, the Drive folder, the GoDaddy login and the Stripe account — none of which he can be given through the product.

When a customer cancels

  1. Check the term

    If they are inside a 6 or 12 month minimum, the early-termination fee applies — 50% of remaining fees, or 30% after month six on a 12-month term. Notice is 30 days and does not shorten the minimum term.

  2. Do not delete anything

    Set the venue to paused. Deleting erases every tap, rating and complaint permanently and cannot be undone. Pausing keeps the plaques valid if they come back.

  3. Recover the plaques

    Nudge-branded plaques are our property and are due back within 14 days, or invoiceable at replacement cost. Custom-branded plaques stay with the customer.

  4. Stop the billing

    Manually, since Stripe isn't live. Once it is, cancelling the subscription flips the account automatically and the links go dark on their own.

15 What has to be revisited, and when

Nothing on this list is urgent today. Everything on it becomes expensive if it is never done.

WhenCheckWhy it is on the list
WeeklyThe operator console error logIt is the only place a crash surfaces. Nothing alerts anyone
WeeklyPrivate feedback across all venuesAlert emails cannot send yet, so a complaint sits unread unless someone looks
WeeklySubmit the enquiry form on nudgetags.com and confirm the email arrivesThe free relay can silently stop delivering while still showing the visitor "Got it"
MonthlyFinance close — see the playbookThe books are only as good as what was typed in
MonthlyGoogle Cloud billing and quota, once a key existsOne billed call per venue per day; the cost grows with the customer count and nothing caps it
MonthlyThat the two scheduled jobs actually ranA failed run is silent — no retry, no alert, nothing in the error log
QuarterlyTap a real plaque and confirm Google's review box still opensThe deep-link format is undocumented. If Google changes it, every plaque on every counter quietly stops converting
QuarterlyRead the marketing site, the FAQ, the contract and the app's Terms side by sideThey currently disagree about GST, lock-in, plaque ownership and the liability cap
QuarterlyWho is in SUPERADMIN_EMAILS, and who has Drive, Vercel, Supabase and GoDaddy accessAccess accumulates. This is also the list to check the day anyone leaves
QuarterlySupplier pricing and freightShipping is 29% of spend and the last quotes are from August
YearlyDomain renewal and the card on file at GoDaddyA lapsed domain takes the site, the app, the plaques and the email at the same moment
YearlyThe Stripe API version pinned in the codeStripe deprecates versions; ours is pinned deliberately and will need moving
YearlyLegal page dates and the privacy claimsThree pages carry a hardcoded date. The privacy page says "the two of us" — which stops being true the first time a contractor gets access
YearlyCached Google addresses and photosNothing re-syncs them. A venue that moves keeps its old address forever
At $75k turnoverGST registrationThen flip the GST flag in the quote tool, and fix the three documents that already claim GST is included
Before venue #100The daily rating job's 60-second budget, and the monthly report's lack of a send logBoth work fine now and both fail quietly at scale
On any suspicionRotate AUTH_SECRETIt is the only way to sign a stolen session out. There is no device list and no per-user revocation

16 Open items, worst first

Everything found in this audit that a founder would rather know now than discover later. Nothing here is on fire; several things would be, given time.

Do first

WhatWhy it mattersEffort
Put the code in a private GitHub repo There is no version control anywhere. No history, no undo, no way to see what changed between two deploys, and the only copies are one Mac and a Drive backup of it. Everything else on this page is recoverable; this is the one that isn't. 1 hour
Check whether the Supabase tables are publicly readable Supabase exposes tables in the public schema over its own API by default, and none of our 13 tables have row-level security. The app never uses that path — it connects directly — so this has simply never been thought about. If the table editor shows "Unrestricted", customer data may be readable, and writable, with a key that is public by design. Verify before anything else on this list. 15 min to check
Finish moving quote.nudgetags.com Done 20 Aug A redirect now exists in the product's config sending that hostname to the operator-only builder — but it cannot fire, because the domain is still attached to the old nudge-tags-quote project (verified 19 Aug). So the public calculator is still live, with no login and no noindex, displaying our landed cost, our markup and our margin to anyone with the link. Detach the domain from that project and attach it to nudge-reviews, then the redirect takes over. 10 min
Strip Purple Pi purple out of the Build Guide The shipped "Build Guide and Shopping List" document — in Drive where Leon can open it — contains 23 occurrences of the PPM brand purple, set by the two scripts that generate it. That is a direct breach of the rule that nothing Nudge-facing carries a Purple Pi mark, inside a 50/50 business. The same document also carries a purplepimarketing.com link and still instructs printing QR codes. 20 min
Retire the old pitch PDF It is in Drive › Sales, it prices $49/$99 tiers that no longer exist, says prices include GST when we are not registered, promises no lock-in against a contract with an early-termination fee — and it tells the customer "they cost us a few dollars each". That last line is an unforced margin disclosure in a sales document. 5 min
Delete the runaway line in migration 006 It was written as a one-time back-fill but migrations are re-run every time, so it now silently promotes any correctly-restricted team member to see every venue in their business — no log, no symptom. 5 min
Fix the menu links on venues that run both A purpose-built menu route exists and works, but nothing links to it — every table QR is generated pointing at the review path. On a venue running reviews and menus, a customer scanning a table code gets the five-star page instead of the menu, and the tap is filed as review traffic. Menu-only venues are unaffected, which is why it hasn't been noticed. Small code fix
Fix the "Edit access" form It never writes the field that controls whether a member sees all venues. Change someone from one venue to "all" and they end up seeing nothing, with no explanation; change them from "all" to one venue and they keep seeing everything while the screen says they're restricted. The second one is a real privacy failure in exactly the multi-venue scenario the feature is sold on. Small code fix
Test the enquiry form end to end The relay needs a one-time activation link clicked in the mailbox, and an un-activated or throttled form still tells the visitor "Got it" while the enquiry disappears. Every web lead depends on this and nobody has confirmed one has ever arrived. 5 min
Switch email on No password recovery for any customer, no feedback alerts, no monthly report — the report being the single best anti-churn asset in the product, which has never sent once. 1 hour

Fix this month

WhatDetail
Reconcile the money documentsGST said three different ways, lock-in contradicted between the pitch and the contract, plaque ownership answered three ways, and no written price ladder to fill the contract's fee schedule from. Retire the August pitch PDF in Drive › Sales — it is the oldest and the wrongest.
Decide the gating policyThe contract promises we do not screen unhappy customers away; the product ships a threshold that does exactly that when set above 1. It defaults to 1 and the site is open about it, so the position is honest today — but it is a setting, not a guarantee, and it is also the main line of attack against the biggest competitor. Make it a decision.
One default for the star thresholdSix code paths currently disagree — three default to 1, three to 4 — and one update path silently falls back to 4, so a partial save can re-arm gating on a venue the owner set to 1.
Fix the four dead "Get started" buttonsThe product's own marketing page sends every sign-up click to a page that says you cannot sign up. That is a straight conversion leak on a live page.
Fix the broken social preview imageFive of the eight marketing pages point at an image that does not exist, so sharing them anywhere shows nothing. One line each.
Fix the circle spec sheet's chipIt says NTAG213; the round plaque is meant to be NTAG215 or better. This is the drawing that goes to suppliers.
Clean the Drive build guideIt carries Purple Pi branding and a purplepimarketing.com link inside a 50/50 business, and still instructs printing a QR code on every plaque.
Rename the Drive "Review Software" folderIt shows a dead Cloudflare build at the top level with the real product buried underneath. Anyone opening it reads the superseded version first.
Pick one outreach trackerThe script says log in one file; the 100 real leads live in another with different columns.
Register the business name with ASICA partnership trading under a non-surname name is required to. About $45 a year.
Write an actual partnership agreementThe signed minutes record 50/50 and nothing else — no profit distribution, no deadlock, no IP, no buyout, no incapacity. Also worth pricing public liability insurance, given you physically install objects in commercial premises as personally-liable partners.
Fix the DocuSign profileIt stamps Gauresh's title as CEO of the wound-down previous company onto documents, and the account is set to US Pacific time.

Know about it

ItemImpact
The Google rating panel renders twice on the Overview when a single venue is selectedCosmetic, but it is the first thing a customer sees
Owner previews inflate the owner's own numbersTapping your own plaque in preview is excluded from tap counts but not from the average rating, happy percentage or "sent to Google". The public badge excludes them correctly, so the two disagree
The period filter does nothing on the private feedback tabIt shows the most recent 200 regardless of the 7/30/90 selection sitting above it
The feedback CSV is hardcoded to Brisbane timeWrong for any interstate venue, in a product that is otherwise careful about timezones
Unique visitors roll over at UTC midnight, but the day column rolls at the venue's midnightIn Brisbane that is 10am, so a customer visiting at 9 and at 11 counts as two people
Appending ?c=preview to any plaque link makes that rating vanishAn open suppression switch. Also means a venue must never name a placement "preview"
A cancelled venue's public ratings badge keeps workingTheir review pages go dark; the badge on their website does not
The locked-tab "Set up" button can dead-loopIf a capability was explicitly switched off, the setup form saves the link but never flips the switch, so you land back on a locked tab
A never-invoiced account can run three live venues indefinitelyThe default venue limit is 3 and an account with no plan stays fully live — deliberate, to cover the gap between an in-person sale and billing, but it means nothing chases an unpaid account
Stripe's "incomplete" status maps to "payment failed"A customer part-way through card verification would see a red "your last payment failed" banner before they had ever paid
Taps, ratings and complaints are never deletedIncluding any contact detail a customer typed into a feedback form. The privacy pages promise retention windows that nothing enforces
One analytics query can never return a rowThe "runs both / reviews only / menu only" mix on the Overview filters on the wrong value, so that panel is permanently empty
Marketing site has no analytics at allDeliberate and honest — and it means there is zero measurement of traffic or form conversion. Worth being a choice rather than an accident
A domain we may not own is baked into a print scriptnudgetags.link is hardcoded as the QR base in the card-printing script. Nothing live uses it, but anything printed from that script would point at a domain that does not resolve
Leon cannot regenerate any artworkThe Drive Brand folder has only one of the six generator scripts. The plaque and spec-sheet generators exist on Gauresh's Mac alone, and need macOS, a specific system font and the Drive mount under his account
A stray Supabase link file names a different projectThe folder carries a leftover command-line link to a second, probably abandoned Supabase project. Anyone running a Supabase push from that folder would target the wrong database. Delete the folder or re-link it
Personal data sits in a personal DriveThe signed partnership minutes carry both founders' home addresses and dates of birth, in Drive under one personal Google account
All three websites bill to the Purple Pi Vercel teamAccepted deliberately — a second team needs a payment method and nothing customer-visible carries the name — but it does mean a 50/50 business's entire hosting sits in one partner's other company, with no access for the other
Two dead codebases still on diskAn early redirect-only Cloudflare worker and a full Cloudflare version of the review product. Neither was ever deployed; both have placeholder credentials. Salvage the QR encoder and the compliance write-up, then delete
Things this audit could not verify from the outside Whether the Supabase tables are exposed over the public API · the exact live value of SUPERADMIN_EMAILS · whether the enquiry form has ever delivered an email · whether the Vercel plan supports the monthly report's 300-second budget · whether .env.production.local, which holds the live database password, is genuinely excluded from the Drive backup of that folder. Each is a five-minute check and each has a real consequence.

17 Where everything lives

Which file, which folder, which document. Start here when you can't remember where something is kept.

On the Mac

Everything is under ~/Documents/claude/nudge-tags/. This folder is backed up to Google Drive and is not in version control.

FolderWhat it isStatus
nudge-reviews/The product. Next.js app behind app.nudgetags.com — dashboard, tap pages, operator console, finances, quote builderLive
website/The marketing site. Eight hand-written HTML pages plus the WebGL showroomLive
quote/The standalone quote calculator, one HTML fileBeing replaced
ops/This pageLive
brand/Brand marks and the plaque artwork generators, plus the brand guideLive
renders/The hand-written 3D renderer that makes the product imagesLive
agreement/The rendered Service AgreementLive
nudge_contract.jsThe contract generator. Edit this, never the documentLive
outreach/The three-email post-visit sequence and its rulesLive
pitch/The old venue pitch deckRetire it
cards/Print sheet for insert cards, for the retired stand formatDead
worker/The first redirect-only Cloudflare build. Never deployedDelete
nudge-review/The full Cloudflare version of the review product. Never deployed. Worth salvaging its QR encoder and its compliance write-up firstDelete after salvage
…/nudge-tags-accounting/A stale local export of the accounting sheet. Not the bookIgnore

In Google Drive — BUSINESS › Nudge Tags

FolderWhat is in it
AccountingThe accounting sheet — the book of record — and the supplier invoices
BrandLogos in SVG and PNG, the palette, the brand guide. Missing five of the six generator scripts
RendersThe source of truth for artwork. "Circles — General (not reviews)", "Squares — Review Plaques", "Colour Codes". Print masters, spec sheets and 3D renders
SalesThe Service Agreement, the (retire-it) pitch PDF, the outreach script
LegalThe signed partnership minutes and the DocuSign certificate
WebsiteA copy of the marketing site and the links-and-hosting document
Review SoftwareMisleading. A dead Cloudflare build sits at the top level with a stale copy of the real app underneath. Rename it
Quote CalculatorA copy of the standalone calculator
BuildThe build guide — the one with Purple Pi purple in it
OperationsThis manual and its supporting documents
Documents whose link is the asset — never delete and recreate these Four Google Docs are referenced from elsewhere, so recreating one breaks every link to it. Edit them in place.
· Nudge Tags — Website, Links and Hosting
· Nudge Tags — Email Outreach Script (Post-Visit)
· Quick Suite vs Nudge Tags | Pricing & Strategy Comparison
· Quick Suite | Competitor Dossier

18 Change log

What changed in this manual, and when. Append a line here every time it is updated — a manual nobody trusts the date on is a manual nobody reads.

DateChange
20 Aug 2026 quote.nudgetags.com secured. The domain was moved onto the app, so its redirect to the operator-only /quote builder now fires — every path lands on the admin login. The retired nudge-tags-quote project, which was still serving the old calculator publicly on its own vercel.app URL complete with landed cost and the 3× multiplier, now serves a stub with no pricing in it that redirects the same way. Verified: both URLs 307, and neither response contains cost or markup.
20 Aug 2026 Placeholder URLs highlighted, with worked examples. Every URL containing a per-venue placeholder is marked in amber in section 04 and carries a real example — four of which are live links to the demo venue.
20 Aug 2026 Added "Every URL, complete" (section 04) — every public page, customer link, signed-in screen, operator surface, machine endpoint, redirect and console in one place, including the two generated pages a close produces (/welcome/<slug> and /thanks/<slug>) which had never been written down. The standing rule is stated at the top of that section: a new URL gets recorded here and in the Drive links document in the same session.
20 Aug 2026 Plaque prices moved to a published price list. Regular review cards $15, or $10 at ten and up; custom review cards $20, or $15 at ten and up; installation unchanged at $50 flat. Prices are no longer derived from landed cost — a supplier price change now moves our margin instead of the customer's quote. The band is judged on the family total, so colours count together. The quote builder shows the full price list on screen, marks which band each line landed in, states the bulk saving in dollars, and prompts when one or two more units would drop the whole family to the next price. The old standalone calculator now carries a do-not-quote banner.
19 Aug 2026 Finances tab state now survives a refresh, and the load got faster. The tab, time range and ledger search ride in the URL (?tab=revenue&range=6m), read on the server so a reload paints the tab you were on rather than flashing Overview. The wait was never the loading screen — it is one call to the Apps Script feed — so a fetched payload is now reused for 8 seconds, which makes a refresh, and the hop between /finances and /quote, effectively instant. The 60-second poll always reads fresh.
19 Aug 2026 Quote builder repriced per variant. Price per plaque and the install fee are no longer input fields — each product is priced from its own landed cost (3×, rounded up to 50c, $5 floor), the quote shows a line per selected variant, and the margin panel breaks down profit and margin per variant. Sale rows written to the Income sheet now carry each variant's actual price rather than a pro-rata share of a blended one.
19 Aug 2026 First full audit and first version of this page. Every codebase, asset, Drive folder, live surface, environment variable and business document read end to end. Verified live: DNS records, Vercel projects and their production environment variables, the accounting sheet's current shape, and which project owns each domain.
How to keep this page true The page is plain HTML assembled from files in ~/Documents/claude/nudge-tags/ops/parts/. Edit a part, run ./build.sh, then vercel --prod. Add a line above whenever you do. If a section is out of date, it is worse than missing — say so in the text rather than leaving it looking current.