No. 04

Platform · 2026

The Honest Toke

A business directory and map where owners run their own listings and catalogues, shipped as a PWA.

Outcome

Took a co-founder's idea to a live directory of 1,035 businesses, where owners run their own listings and catalogues, with a map, shipped as a PWA.

  • 1,035

    Businesses listed

    Counted in the production database on 28 Sep 2026.

At a glance

Role
Co-founder and lead developer
Client
The Honest Toke
Engagement
Venture
Timeline
Team
Solo
Stack
  • Frontend Next.js, React, TypeScript, Tailwind CSS, shadcn/ui, TanStack Query, Motion, Google Maps Platform
  • Data Supabase

Context

The Honest Toke is a directory and map of cannabis businesses in South Africa: stores, social clubs, wellness shops and grow shops. Consumers find businesses near them, leave reviews, save favourites, and follow events, forums and news. Business owners claim or register their listing and run it themselves, including a product catalogue. The idea was my co-founder Wernich's. There was no product when we started in June 2026. I built Phase 1 from the first commit, and it runs at thehonesttoke.com as a progressive web app.

My role

I'm a co-founder and the lead developer. Wernich and I are equal co-founders: he owns the business side and marketing, and I own the product's technical side. I was the only engineer, so in Phase 1 I did all of the engineering:

  • the technical decisions, and scope and priorities with Wernich
  • the Next.js front end, the Supabase data model, its access rules and file storage
  • the map, geocoding and the import of the business data
  • QA, testing, CI and project management

Problem

A directory is only useful if its details are right, and the people who know the details are the owners. Trading hours, contact details and stock change, and a list run by admins falls behind. So owners had to be able to run their own listings. That moves the hard part to trust. A listing carries signals consumers rely on: SAHPRA verification, a premium flag and a rating built from reviews. An owner needs to edit their hours and catalogue. They must not be able to set those signals, take over someone else's listing, or replace another business's images.

Constraints

  • One engineer. I built, tested and shipped Phase 1 alone between June and September 2026, so anything I built, I also had to test and support alone.
  • Free and low-cost tiers. Supabase's free plan, Vercel's Hobby plan and Google Cloud's monthly free credit. The free Supabase plan has no leaked-password protection, so we accepted that gap.
  • Messy source data. About a thousand businesses came from a research spreadsheet. Most rows had a name and a town but no street number, and trading hours were free text.
  • A regulated domain. The Cannabis for Private Purposes Act of 2024, POPIA and PAIA shaped the policy pages, the data-usage page and account deletion.

Approach and architecture

The app is Next.js with the App Router, deployed on Vercel. Supabase provides Postgres, auth and file storage. TanStack Query handles fetching and caching on the client. Google Maps Platform draws the map, searches places and resolves addresses.

Getting the businesses in

A command-line import reads the spreadsheet and maps each row's type to a category and amenities. It parses trading hours into structured data, gives each business a unique tag and cleans phone numbers to the +27 format. It then geocodes each business through Google, using the name and town when the street address is missing, and fills in the address Google returns. Coordinates are stored on the business row as a PostGIS point. A refresh script re-geocodes when needed, so the map reads stored coordinates and never geocodes on a page view.

Owners and ownership

Every business row has an owner. An unowned listing can be claimed: the owner sends a request, and an admin approves or rejects it. Approval sets the owner, upgrades the account and closes the request in one database function. Row level security lets owners edit their own listing and catalogue items, and nobody else's. Guard triggers stop owners changing the trust columns, and storage policies limit logo and banner changes to the uploader. If an owner deletes their account, the listing goes back to unclaimed instead of disappearing.

The shell

The app ships as a PWA, with a web manifest and a full set of Apple splash screens, so people can install it on their phones.

Key decisions and trade-offs

Owners run their listings through a claim flow

Chose
Owner claims, approved by an admin
Over
A directory curated only by admins
Cost
An approval queue, and the highest-privilege write in the product

Owners know when their hours or stock change, and two people could not keep more than a thousand listings current. Admin approval keeps a person between a claim and a handover, which matters when a listing carries a verification badge. Approving a claim hands a business to an account, so it has to be atomic. My first version ran three separate writes. I later moved them into one transaction, so two admins approving at once can't both succeed.

Google Maps Platform replaced the first map

Chose
Google Maps Platform for the map, geocoding and place search
Over
Leaflet with OpenStreetMap tiles and Nominatim geocoding
Cost
A billed API, restricted keys, and local development tied to port 3000

My first map, in July, used Leaflet with OpenStreetMap tiles, and Nominatim for geocoding with city centres as the fallback. In September we imported the spreadsheet, where most rows had no street address. Google could resolve many of those from the business name and town. A city centre fallback would have stacked those pins on one point. The Places API also gave the map search by suburb and city.

A PWA now, a native app later

Chose
An installable progressive web app
Over
A native mobile app in Phase 1
Cost
No app store listing, and native features wait for Phase 3

One engineer could not build and support a web app and native apps at the same time. A PWA let people install the app from the browser on one codebase. The README keeps a native app, built on the same backend APIs, for Phase 3.

Outcome

Phase 1 is live. On 28 September 2026 the production database held 1,035 business listings. Most came in through the import, and owners claim and update them from there. Consumers can find businesses on a list or a map, review them, and follow events, forums and news. Owners can claim or register a listing and run it and its catalogue. The access rules are tested against a real Postgres database in CI. A pgTAP suite covers privilege escalation, storage ownership, the claim flow, and rules that used to live only in API routes. Phase 1 is now in long-term support.

Reflection

The call I'd reverse is how I handled the database rules. For most of Phase 1 there were no migrations: policies were changed directly on production, and the tests ran against a mock that doesn't enforce row level security at all. A security triage on 17 September found real gaps in production. A signed-in user could make themselves an admin, owners could set their own verification and premium flags, and anyone could overwrite anyone's uploaded files. I fixed and applied them that day, then added a migration baseline and the pgTAP suite. Next time the migrations and the policy tests come first, before the first table owners can edit.

If owners can edit it, the database has to say what they may change, and a test has to prove it.

Phase 2 starts in October 2026 and adds payments, purchases and events affiliates. Those are plans, not results.

Creation of Adam

Interested in working together?