Tray: a trading toolkit for small gemstone dealers
Product and design plan. No code.
Working name Tray (placeholder: dealers shoot on trays, and a memo goes out on one). Plan only, no code. Written as senior PM and product designer. Companion file: the seven key screens, drawn with the design system tokens (final design · first prototype sketch).
0. Decisions and assumptions I made so you don't have to answer them
- Same backend as GemBridge, one account, one future brand. Decided by April, 13 October 2026: Tray lives in the same Supabase project as GemBridge, shares its auth, and both sit under one umbrella brand later ("Gemify" or similar). So: one login for a dealer whether they use the toolkit or sell through GemBridge, one
vendors/dealersrecord, and the stones a dealer already sent GemBridge (418 rows insupplier_inventorytoday) become their starting inventory instead of a retype. What stays separate by rule, not by database: a dealer's cost prices, client list and sales are private tables that no GemBridge code path reads. Sharing stones into the GemBridge buyer network is a per-stone opt-in switch next to the catalog switch, off by default. Section 11 says how the row-level rules keep the two products apart inside one project. - Android matters as much as iPhone. Thai dealers lean iPhone; Sri Lankan, Indian, Afghan and African dealers lean Android. Flutter covers both, so no change, but both get tested from day one.
- Thai UI is not "later". English is fine for the prototype, but every string goes through translation from the first commit and Thai ships in phase 3. Many Thai shop owners will not adopt an English-only tool.
- Prototype data stays on the phone first. The first build computes everything on the device. Cloud sync arrives in phase 2. This is faster to build, works offline by default, and makes the trust conversation easier ("your list is on your phone until you press publish").
- Prices are assumptions I'll test, not facts. Section 2.
1. The user, the problem, and where I agree or push back on the framing
The user. A one-to-five person shop in or around JTC. The owner holds the stock, the client relationships and the prices in their head. Phone-first, WhatsApp all day, Excel at best. Often not a native English speaker. They are paid on the next sale, not on tidy records, so any tool that adds work before it adds a sale is dead on arrival.
What I agree with, unchanged
- Inventory is the core, and the real competitor is "photo from the camera roll, forwarded on WhatsApp". The bar is: faster than that.
- The paper memo slip is the sharpest pain. Stones disappearing on consignment is money, not inconvenience, and nothing replaces the slip today.
- Colored stones are one-of-a-kind and non-standard. Diamond tools do not fit, and a field-heavy ERP does not fit.
- The catalog is the sticky part and the dashboard is the talked-about part.
Where I push back
- "Thousands of dealers" is a ceiling, not a market. JTC is reported to hold roughly 300 shops (a travel-site figure, unverified; the building also houses the AIGS and GRS labs and the Thai Gems & Jewelry Traders Association). Add Mahesak, the Silom and Surawong side streets, and the weekend Chanthaburi traders, and a reachable Bangkok number is plausibly 1,000 to 2,500 trading entities. Count doors in week one. Below ~800 reachable shops this is a good small business, not a venture-scale one; the cross-dealer data is the only part that scales beyond it.
- Dealers hide stock from other dealers. In this trade a visible stock list is a negotiating weakness: competitors see what you hold and what you can't move. "Public by default" will scare the very people we want. I keep the brief's default (public specs, no prices) because it is what makes a link shareable, but onboarding asks one plain question per dealer, "Who can see your catalog?", with three icons: anyone with the link / only my clients / nobody yet. The whole-catalog lock is a first-class option, not a footnote, and I expect a third to half of dealers to pick it.
- A memo is a legal and trust object, not only a list. In the trade, title stays with the consignor and the stones are usually at the receiver's risk. A slip with photos is a real improvement, but a disputed memo is decided by who can prove what was handed over. So the slip must be a frozen snapshot (section 4) with its own link, and "receiver confirms" is not a later-version luxury; I'd pull a one-tap "I received these" link into phase 3.
- $20 to $50 a month is believable only if the first value is free. Dealers will not pay before they have seen it work on their own stones. Pricing is built around that (section 2).
- Scope. The v1 list is eight app features plus a client web app with auth. That is a large product. The prototype (section 3) is deliberately a quarter of it.
- The biggest copycat risk is not GemCloud. It is WhatsApp Business's own catalog feature (product catalogs and collections, inside the app dealers already live in). It has no stone-specific fields, no per-client price visibility, no consignment, no dashboard, and caps item counts, but it is free and one tap away. We must feel better than that, not just different.
Competitors, assessed honestly (limited web check on 2026-10-13; verify with real dealer interviews before relying on it)
| Alternative | Who it serves | Cost | Why a one-man JTC shop isn't on it |
|---|---|---|---|
| Excel + WhatsApp | Everyone | Free | It is the incumbent. It wins on speed and zero setup. It loses on search, memo tracking, numbers. |
| GemCloud | Gem and jewelry businesses; listed availability Canada, Hong Kong, United States; founded 2018; barcode labels, consignment tracking, purchase orders, reporting | Listed from ~US$90/month (Capterra; FitGap lists ~$76). Treat as unconfirmed | At ~3x our price point, desktop-style workflows built around barcodes and purchase orders, no local presence or onboarding in Bangkok. I found no evidence of WhatsApp-native sharing or client catalog logins; I could not confirm their absence. |
| Diamond tools (RapNet, GemFind) | Diamond dealers | Subscription | Built on a grading scale colored stones do not have. |
| Jewelry ERPs (JewelMate, Jewel360, Zoho, Odoo) | Retail jewelers with staff | Monthly + setup | Too many fields, desktop first, for a business that is one person and a safe. |
| WhatsApp Business catalog | Any small merchant | Free | Not stone-aware, no private prices per client, no memo, no numbers. The real benchmark. |
| Marketplaces | Buyers hunting stones | Commission | Dealers do not want to be commoditized or have their margins visible; this is the opposite of "my clients, my prices". |
2. Business case
Who pays and how much. The dealer pays. Clients never pay.
| Plan | Price | Includes | Why this line |
|---|---|---|---|
| Free | $0 | App, up to 50 stones, WhatsApp share cards, consignment slips, basic dashboard | Wins the camera-roll fight. Everything a dealer does daily is free to start; a free user still shares share cards, which is our marketing. |
| Pro | $29/month (or ~฿1,000) | Unlimited stones, web catalog with client logins (up to 30 clients), full dashboard and suggestions, import | One extra sale pays for years. Catalog and numbers are the things dealers would pay for. |
| Pro Plus | $49/month | Unlimited clients, price tiers per client (later), second user (later), custom domain (later) | Bigger shops, grows with the product. |
Collection: bank transfer or PromptPay QR for Thai dealers, invoiced by hand for the first 20, card (Stripe) for abroad. Do not make Thai dealers fight a foreign card form on day one.
Segment size and revenue, with the arithmetic visible (all assumptions):
| Scenario | Reachable shops | Paid conversion | Revenue/month | ARR |
|---|---|---|---|---|
| Bangkok only, modest | 1,000 | 15% | 150 x $32 = $4.8k | ~$58k |
| Bangkok only, strong | 2,000 | 30% | 600 x $35 = $21k | ~$250k |
| Plus Colombo, Jaipur, Hong Kong, Dubai (3 years) | 6,000 | 20% | 1,200 x $35 = $42k | ~$500k |
Honest read: a solid lifestyle business, with room to be more only if the data layer (v2 and v3) becomes something others pay for.
First 20 dealers at JTC. This is door-to-door, not ads.
- Start with people who already trust you. The GemBridge network has dealers who have already sent you inventory (the Sri Lankan sapphire suppliers with 380 and 24 stone lists, the Mogok spinel and Ceylon sapphire sellers, the emerald dealer). Because Tray shares the GemBridge backend, those stones are already in the database: their first login shows their own stock, with photos still to add. The pitch becomes "your list is already in; here is your catalog link". That is 5 design partners, free for 3 months, in exchange for weekly feedback and permission to use their catalog link as a demo. One care point: those stones were sent to GemBridge for matching, so they start with
share_with_networkon and the dealer is told so plainly at first login, with the switch right there. - Floor by floor at JTC. Walk with a phone showing a real catalog (a design partner's, with permission). The pitch is 20 seconds: "Someone asks 'what's new?' You send one link." Not "inventory software."
- Share cards as the loop. Every share card carries a small "made with Tray" line. Buyers who receive a clean card ask the dealer what it is; this is how it spreads building to building. Dealers who lose a sale because the stone wasn't findable are the best converts, so I ask early adopters about their last lost sale.
- Two reference dealers per wing. Trade is word of mouth within a few blocks; a neighbor using it is worth more than any pitch.
- Benchmarks for "20". 5 by week 6 (design partners), 12 by week 12, 20 by week 20. Success is weekly use, not installs: at least 12 of 20 open the app on 4+ days a week by week 20. If that fails, we do not scale; we fix.
What stops a bigger player copying it. Features copy easily; these do not:
- Catalog lock-in. Every client who has a login on a dealer's catalog is a re-onboarding cost to leave. Dealer data export stays free, but the client relationships are the glue.
- Density in one building. If a third of JTC is on Tray, new buyers see the same look everywhere and expect it. A foreign vendor cannot buy that with ads.
- WhatsApp-native, plain-language, phone-first design. Incumbents are desktop or ERP companies; their org chart cannot ship this without rebuilding.
- Data. Structured colored-stone inventory with media, certificates, prices and sell-through across hundreds of dealers does not exist. It is a slow moat, but real.
- Local presence. You can be in the building. They cannot.
Why dealers stay. The catalog (their clients are in it), the memo history (the only record of who held what, ever), and the dashboard (once they know their numbers, they won't go back to a feeling).
What the GemBridge link adds. Two things, both real but second-order. First, distribution: around 31 active dealers are already in the GemBridge network, several with inventory on file; they are the warmest list for the first 20 and the import is already half done. Second, a reason to pay that no competitor has: a stone marked "share with GemBridge buyers" can be matched to a real buyer enquiry. That is the one feature GemCloud or WhatsApp cannot copy, and it belongs in Pro, not Free. The order of operations matters though: the toolkit must earn its place on the phone on its own, with the sharing switch off, or dealers will read it as a marketplace in disguise and hide their stock (push-back 2 above).
3. Prototype scope
Prototype goal: one dealer, 30 real stones, one real memo, one real share card, in 14 days of use. Build the smallest thing that can fail that test.
| In the prototype | Why |
|---|---|
| Add stone: camera first, short form, smart defaults, offline | The core loop. If this isn't faster than WhatsApp, nothing else matters. |
| Stock grid, stone detail, search and filter (type, carat, color, shape, origin, treatment, price, status) | "Find any stone in seconds." |
| WhatsApp share card (one stone or a selection, with or without price) | The product's marketing, and the fastest visible win. |
| Clients: add by hand or from phone contacts; a wishlist text field | Memo needs a client. Wishlist matching waits. |
| Consignment: pick client, pick stones, date, slip to WhatsApp, per-stone outcomes, sale from consignment | The sharpest pain; the reason a dealer might switch. |
| Sale: link stone to client with price, currency, date | Closes the memo loop and feeds the numbers. |
| Dashboard: Needs attention (overdue and due-this-week memos, stones missing photo or price) and Performance (6 numbers, below). No Suggestions block yet. | Two blocks are computable on the device from five tables. Suggestions need enough history to be right; a wrong suggestion on day one costs trust. |
| Concierge import: I load the dealer's sheet for them | Tests the value without building the import UI. |
| Catalog, public only, read-only, generated from the same data; no logins | A static page per dealer shows the "what's new becomes a link" moment and checks the media pipeline. Logins are the most complex part and are not needed to prove the idea. |
| Out of the prototype | Why |
|---|---|
| Client logins, view tracking, price unlock | Largest build and security risk; phase 2. |
| In-app Excel import | Concierge first, learn the real messy sheets, then build it. |
| Suggestions, cross-dealer data, external references | Need data and trust first. |
| Sync, multi-device, backup | Phone-only first; backup (not sync) lands in phase 2. |
| Thai UI, custom domains, multi-user | Later. |
Prototype dashboard numbers: stones by status, stock value at asking, value out on consignment, sold this month and last, stock older than 90 and 180 days with its value, average days to sell. Everything else waits.
4. Data model
Postgres in Supabase; mirrored by a local SQLite database in the app. Ids are UUIDs created on the device so offline records have stable identity. Every table has dealer_id, created_at, updated_at, deleted_at (soft delete), and version.
Principles that shape it
- Events for anything that moves stock. A stone's status and quantity are derived from an append-only
stone_eventslog. This is what makes partial lot sales, split lots, memos and offline merges safe. - Private data lives in separate tables. Cost prices are never in the same table as anything a client or the public can read. Row-level security is per table, not per column, so separate tables is the only safe design.
- Memos are snapshots. A slip is frozen at creation and cannot change; later changes create a new version.
| Entity | Key fields | Notes |
|---|---|---|
dealers | id, auth_user_id, vendor_id (→ GemBridge vendors), name, shop_name, slug, whatsapp_number, default_currency, display_currency, catalog_mode, show_prices_publicly, reserved_display, plan | One row per shop (v1: one user). vendor_id links the toolkit account to the existing GemBridge dealer record by phone, so one person is one account; a dealer who never joins the network has a NULL there. catalog_mode: public / clients_only / off. |
stones | id, dealer_id, ref (short human code, e.g. R-0142), kind, parent_id, species, variety, color_text, color_grade, clarity, shape, treatment, origin, carat, pieces, length_mm, width_mm, depth_mm, cut_quality, certificate_id, status, in_catalog, share_with_network, supplier_inventory_id, notes, added_at | kind: single / pair / suite / lot / parcel. A lot's parent_id is NULL; split children point at it. share_with_network is the per-stone GemBridge opt-in, default false. supplier_inventory_id points at the row a stone was seeded from, so the existing 418 stones migrate once and never duplicate. Over time stones replaces supplier_inventory as what the GemBridge matcher reads, through a view limited to share_with_network = true. |
stone_pricing | stone_id, asking_amount, asking_currency, basis (per_carat / per_piece / total), price_note, price_visible_override | Readable by granted clients, subject to dealer rules. |
stone_private | stone_id, cost_amount, cost_currency, source_name, purchase_date, private_note | Dealer-only. Never exposed through any view a client can use. |
stone_events | id, stone_id, type, qty_pieces, weight_ct, ref_type, ref_id, at, device_id | Types: added, split, merged, reserved, released, memo_out, memo_returned, sold, withdrawn. Remaining quantity is the sum of events. |
media | id, stone_id, kind (photo / video / cert_scan), storage_key, derived_keys, width, height, duration_s, sort, is_cover, upload_state | Original kept; derivatives generated. upload_state: local / uploading / done. |
certificates | id, stone_id, lab, report_number, report_date, verify_url, scan_media_id, summary_json | Lab allowed values below. verify_url built from lab and number where the lab supports it. |
clients | id, dealer_id, name, company, kind, whatsapp, email, country, language, notes, source, last_contacted_at | kind: dealer / jeweler / collector / broker / other. Phone contact import maps here. |
wishlists | id, client_id, species, color_text, origin, treatment, carat_min, carat_max, shape, budget_max, currency, note, active | One client, many wishes. Matching is a query, not a feature. |
client_access | id, client_id, dealer_id, identity_id, status (invited / active / revoked), invited_at, first_login_at, last_seen_at, revoked_at, price_tier (later) | One row per dealer-client grant. |
client_identities | id, auth_user_id, whatsapp, email, display_name, buyer_id (→ GemBridge buyer, nullable) | The person who logs in. One identity can hold grants with many dealers, so a buyer uses one login across shops. Dealers never see another dealer's data. If the same person is also a GemBridge buyer, the link is by phone and is informational only; a GemBridge buyer never gets catalog access without the dealer granting it. |
consignments | id, dealer_id, client_id, ref, created_at, due_date, note, status (open / overdue / closed), closed_at, snapshot_json, snapshot_hash, slip_token | overdue is derived, not stored. snapshot_json is the frozen slip. |
consignment_items | id, consignment_id, stone_id, qty_pieces, weight_ct, price_at_memo, currency, outcome (out / returned / sold), outcome_at, sale_id | Per-stone outcomes. Partial returns are normal. |
sales | id, dealer_id, client_id, sold_at, currency, total_amount, payment_method, note, source_consignment_id | |
sale_items | id, sale_id, stone_id, qty_pieces, weight_ct, price_amount | A lot sold in part has a sale_item with fewer pieces than the lot. |
catalog_views | id, dealer_id, client_access_id (nullable), anon_session_hash, stone_id (nullable), path, at, duration_s | Anonymous rows are counted, never identified. |
dealer_metrics_daily | dealer_id, date, plus metric columns | Nightly snapshot, powers trends. |
share_events | id, dealer_id, stone_ids, client_id (nullable), with_price, at | Records what was sent; feeds "did this client see it?" |
suggestion_log | id, dealer_id, rule_id, shown_at, acted, dismissed, feedback | How we learn which suggestions are wrong or obvious. |
Allowed values (stored as codes, shown with plain labels and icons; "other" always allowed, and "other" values are reviewed monthly to extend the lists)
| Attribute | Allowed values |
|---|---|
| Species | ruby, sapphire, spinel, emerald, tourmaline, garnet, tanzanite, aquamarine, alexandrite, chrysoberyl, zircon, peridot, topaz, morganite, beryl (other), kunzite, opal, moonstone, amethyst, citrine, other |
| Variety (conditional on species) | Sapphire: blue, yellow, pink, padparadscha, orange, green, purple, white, color-change, star. Tourmaline: Paraíba, rubellite, indicolite, chrome, bi-color/watermelon. Garnet: tsavorite, demantoid, spessartite, rhodolite, pyrope, malaya. Ruby: standard, star. Spinel: red, pink, cobalt blue, purple. Beryl: emerald, aquamarine, morganite, heliodor. |
| Color term (optional free pick) | pigeon blood, royal blue, cornflower, vivid, pastel, intense, medium, light, padparadscha-type, neon, mint, hot pink, other (free text) |
| Clarity | eye-clean, slightly included, included, heavily included |
| Shape | oval, cushion, pear, round, octagon, emerald cut, square, rectangle, heart, marquise, trillion, radiant, cabochon, sugarloaf, bead, free-form, rough |
| Origin | Burma (Mogok), Ceylon (Sri Lanka), Madagascar, Mozambique, Thailand, Kashmir, Colombia, Zambia, Tanzania, Nigeria, Afghanistan, Brazil, Kenya, Vietnam, Tajikistan, Pakistan, Australia, unknown |
| Treatment | unheated, heated, heated (residue), glass-filled, diffused, oiled (minor), oiled (moderate), clarity enhanced, irradiated, not known/not tested |
| Lab | GIA, GRS, Gübelin, SSEF, AIGS, GIT (GIT/Gem and Jewelry Institute of Thailand), Lotus, IGI, Other |
| Currency | USD, THB, EUR, others as free pick; dealer sets default and display currency |
| Price basis | per carat, per piece, total |
| Status | available, reserved, on consignment, sold (derived from events, never typed in) |
Cross-dealer readiness. stones stores structured attributes, not free text, so aggregation is possible. A separate market_facts table (built nightly, no dealer id, only buckets) would carry: species, variety, origin band, size band, treatment, weeks-to-sell, view counts, inquiry counts. Dealers opt in through terms of service; any cell with fewer than 5 dealers or 10 events is suppressed.
Per-client visibility later. client_access.price_tier plus a client_stone_visibility table (client_id, stone_id, allow/deny) slot in without a migration of existing tables, because catalog reads already go through one policy.
5. Core flows
5.1 Dealer app
Add a stone (target: under 30 seconds with one photo).
- Tap the large camera button on the stock screen (or Home). Camera opens first, on a white-or-black tray preset that sets exposure and crop.
- Shoot 1 to 6 photos; optionally one 10-second video. Tap Done. The stone exists immediately and is saved locally. Upload starts silently.
- One sheet, big fields, carat first on a custom keypad (decimal, two places). Species is a picker showing the last-used one at top. Shape is an icon row. Color, origin, treatment are chips with the last-used pre-selected (heated sapphire from Ceylon becomes one tap).
- Price: amount, currency (last used), basis toggle (per carat / total). Skip allowed; the stone then appears in Needs attention as "no price".
- Done. A toast says "R-0143 saved" with "Share" and "Add another" actions. No confirmation screen.
Find a stone. Search field at the top of the stock grid. Typing "ceylon blue 4" parses as a filter (origin, color, carat 4+). Saved filters appear as chips. Pinch changes grid density. Tap a stone for detail.
Share a stone or selection. From the grid: long-press to select, tap Share. Choose "with price" or "without" (remembered per client or last choice). The preview shows the card exactly as WhatsApp will receive it. One tap opens the WhatsApp contact picker with the images and caption attached.
Create a consignment (full loop).
- Consignment tab > New. Step 1: pick a client (recent first, search, or add new).
- Step 2: the stock grid, available stones only, multi-select with a running count and total weight and value at the bottom. A scan icon accepts reference codes or QR on the parcel envelope (phase 3).
- Step 3: due date (default +7 days, quick chips +3, +7, +14, custom) and optional note.
- Review shows the slip. Tap Send on WhatsApp: the slip card (image plus a link to the frozen snapshot) goes to the client's chat. Stones become "on consignment, with [client]" immediately.
- Days later, the client returns two stones and keeps one. On the consignment, the dealer taps each stone: Returned (stone available again) or Sold (opens a pre-filled sale: client, price from the memo, today, currency) and confirms. Marking "Sold" records the sale and closes that line.
- When every line is returned or sold the consignment closes automatically and moves to history. If the due date passes with lines still out, it becomes overdue on Home.
Record a sale directly. On the stone: Sold button > pick client > price (prefilled with asking) > date (today) > Done. Stock updates itself. For a lot: ask pieces or carats sold; remaining stays available.
Add a client and use the wishlist. Add by name and WhatsApp number or import from phone contacts. A wishlist is one sentence in plain words plus optional chips; saving a new stone checks all wishlists and, if matched, offers "Tell Mr. X" in one tap.
Grant catalog access and revoke. Client profile > Catalog access toggle > the dealer taps Send on WhatsApp (pre-written message with the link) > access shows "invited", then "active" when they log in. Revoke is one tap with an undo toast.
5.2 Catalog
Public visitor. Opens a dealer link from WhatsApp. Sees the grid, filters, stone detail with media, certificate, specs. No prices if the dealer hides them. A small "Ask about this" button opens WhatsApp with the stone reference. The visit is counted anonymously.
Full catalog loop.
- Dealer adds the client in the CRM (or picks an existing one).
- Dealer taps Grant access > Send on WhatsApp. The message carries a personal magic link.
- Client taps the link. It opens in the phone browser, validates the one-time token, and signs them in with a 180-day session. A soft prompt offers "Set a password" and "Save to home screen".
- Prices appear. The client browses, opens the 3.2 ct ruby, plays the video.
- The catalog records one view (client, stone, time, duration).
- On the dealer's Home, Needs attention shows "Mr. X looked at R-0142 twice today". One tap opens a WhatsApp chat with a pre-filled "Hi Mr. X, saw you looked at the ruby..." the dealer edits and sends.
- Client taps "Ask about this stone" at any time; WhatsApp opens to the dealer with the reference.
6. Screens and pages
App
| Screen | Purpose |
|---|---|
| Home (dashboard) | Opens every time. Needs attention, Performance, Suggestions. |
| Stock grid | All stones as photos; search, filters, status chips, selection mode. |
| Stone detail | Full media, specs, price, certificate, history, actions (share, consign, sell, edit). |
| Add stone (camera, form) | Two steps: capture, short form. |
| Filter sheet | Bottom sheet with chips and ranges. |
| Share preview | The card as it will arrive; with/without price toggle. |
| Consignment list | Open, overdue, closed. |
| Consignment new (3 steps) | Client > stones > date. |
| Consignment detail | The slip, per-stone outcome rows, history. |
| Clients | List with access state. |
| Client profile | Wishlist, purchases, open memos, catalog access. |
| Sale record | Quick sheet. |
| Catalog settings | Visibility mode, price switch, per-stone overrides, view log. |
| Import (phase 2) | File picker, column mapping, preview. |
| Settings | Currency, language, backup, export, account. |
Navigation is a bottom bar with five items: Home, Stock, (+ camera), Consignment, Clients. The Catalog settings live under Home's top-right gear and Settings, not in the bar, because dealers touch them rarely.
Catalog
| Page | Purpose |
|---|---|
| Dealer home / grid | The catalog as a photo grid, with filters. |
| Stone detail | Media, specs, certificate, ask-on-WhatsApp. |
| Sign-in landing | Token exchange screen; error states ("link expired, ask the dealer for a new one"). |
| Request new link | A button that notifies the dealer to resend. |
| Locked state | Shown when the whole catalog is locked; explains how to ask for access. |
| 404 for sold stones | Friendly "this stone has found a home" with a link back to the grid. |
7. UI and UX decisions, with reasons
Navigation: five-item bottom bar with a raised camera button. One thumb, no hidden menus. The camera button sits in the middle because adding a stone is the most frequent act and must beat "take photo, send". Home is first so every launch shows the day's picture.
Dashboard layout: one vertical page, three blocks in the specified order, no scroll for the first two. Needs attention shows at most 4 items, then "See all". Performance is a 2x3 grid of plain figures, each with one tiny arrow trend. Suggestions show two cards. The test is five seconds: "what do I do now" is the top block; "how's business" is the middle. Cutting anything that does not drive action is a rule, not a preference.
Add stone: camera first, then one guided sheet. Following Carousell and Vinted: the camera is the doorway, and the form arrives after the photo exists. Fields are ordered by what dealers say first (carat, type, shape, color, origin, treatment, price). Defaults carry from the last stone. A weight scale is not connected in v1; entering carat by keypad is fast enough, and Bluetooth scales are a later request.
Search: typing parses natural trade phrases. "unheated Ceylon blue 3 to 5" becomes chips the dealer can tap away. No query language to learn. Always-visible saved filters ("Unheated Ceylon blue", "Under $2k") for repeat asks.
Share card: one image, one caption. Cover photo square with a thin footer strip, specs in the caption, reference and a link to the stone's page (preview card shows the cover photo). See the screens file. Caption template: Ruby 3.21 ct · Mozambique · unheated · oval 9.1 x 7.0 mm · GRS on the first line, color and clarity line second, price line only if toggled on, then Ref R-0142. Short enough to read without opening. Multi-stone shares send an album with one caption per photo, or one contact-sheet image with refs for large selections.
Consignment creation and the slip. Three steps, each one screen: client, stones, date. The slip is a vertical card: dealer name and logo/wordmark, client name, date out, due date, a table of stones with photo thumbnail, ref, carat, price, total weight and total value, a line for the terms (editable default: "Stones remain the property of [dealer] until paid; receiver responsible for loss or damage") and a QR/link to the frozen version. It is meant to be screenshot-able and legible on a phone. Terms text is the dealer's own; we do not draft legal language.
Catalog access control. Three settings in plain language, each with an icon: Who sees my stones (anyone with link / only my clients / nobody), Prices (hidden / shown to clients / shown to everyone), and per-stone "hide this one". Client access is a toggle in the client row with a state chip: not invited, invited, active, revoked. The dealer never has to understand "magic links"; the app shows "Send access on WhatsApp".
Language and icons. Plain verbs: "Add stone", "Send", "Out", "Back", "Sold". Never "SKU", "inventory item", "entity". Icons carry meaning alongside every label: a stone-shape set for shapes, a hand-with-box for consignment, a tag for price.
8. Dashboard specification
Everything computed on the device from local data in v1 (instant, offline). Server snapshots are added only for trends and cross-dealer work.
8.1 Needs attention (ordered by urgency; each row is one tap to the fix)
| Rule | Data | Formula | Phase | Why they'd act |
|---|---|---|---|---|
| Overdue consignment | consignments, items | due_date < today and any item outcome = out | Prototype | A stone is out past its date. One tap: WhatsApp the holder, or mark returned. |
| Consignment due this week | same | due_date within 7 days | Prototype | Chasing before it is late is polite; after it is awkward. |
| Catalog stone missing photo or price | stones, media, stone_pricing | in_catalog and (no cover photo or no asking price) | Prototype | A catalog stone without a photo or price does not sell. One tap opens the stone. |
| Reserved too long | stone_events | status reserved for more than N days (default 5) | Phase 2 | A reserved stone blocks a sale. Release or confirm. |
| New stock matches a wishlist | wishlists, stones | wishlist criteria match a stone added in the last 7 days | Phase 2 | The client asked for exactly this; message them now. |
| Client viewed catalog or stone | catalog_views | a granted client viewed in the last 3 days; collapsed per client | Phase 2 | Warmest lead signal there is. One tap opens WhatsApp. |
| Conflict | stone_events | a stone has two incompatible events from offline devices | Phase 2 | Prevents the double-sale burn. |
Cut: "birthday reminders", "unread messages", anything that is a notification about the app instead of the business.
8.2 Performance
| Figure | Source | Formula | Phase | Why they'd act |
|---|---|---|---|---|
| Stock value at asking | stones, pricing | sum of remaining qty x asking price, available + reserved | Prototype | The headline number. "How much is on my shelf?" |
| Stones by status | stones | count available / reserved / on consignment / sold-this-month | Prototype | Where my stones are, in one glance. |
| Value out on consignment | consignment_items | sum of price_at_memo where outcome = out | Prototype | The number that scares them, and should. |
| Sold this month vs last | sales | count and value, month-to-date vs previous full month | Prototype | The answer to "how's business". |
| Average days added to sold | stones, sales | mean(sold_at - added_at), last 12 months | Prototype | Tells if pricing is right. Shown with its sample size. |
| Stock older than 90 and 180 days with value | stones | age = today - added_at for unsold stones | Prototype | Dead money. Tapping lists them. |
| Top clients by purchases | sales | sum by client, last 12 months, top 3 | Phase 2 | Who to look after. |
| Stock cost and margin (optional) | stone_private | shown only if cost entered; asking minus cost | Phase 2 | Private; some dealers will never enter it, and that is fine. |
Each figure has one tiny trend arrow (up/down/flat vs previous period) and nothing else. No charts a dealer must interpret.
8.3 Suggestions
Rules: at most 3 shown; every one has "Why am I seeing this?" (the data behind it) and a dismiss with feedback; none shown until the dealer has 20+ stones, because thin data produces wrong or obvious advice.
| ID | Text pattern | Data | Phase | Why they'd act |
|---|---|---|---|---|
| S1 Aged stock | "6 stones over 180 days, worth $42k. Consider repricing or sending on consignment." | stones age | v1 | A concrete list with a value. Buttons: View list / Reprice. |
| S2 Wishlist coverage | "3 clients want unheated Ceylon blue 3 to 5 ct and you have 2 in stock." | wishlists vs stones | v1 | Immediate sale chances. Button: Share with them. |
| S3 Fair prep | "Hong Kong fair in 3 weeks, 14 catalog stones have no video." | hand-curated fair calendar plus media | v1 | Fairs are when decisions are made. Your own fair calendar already exists. |
| S4 Win-back | "Mr. X bought twice in 2025 and hasn't heard from you in 60 days." | sales, clients.last_contacted_at | v1 | A repeat buyer is the cheapest sale. |
| S5 Unpublished stones | "8 available stones aren't in your catalog." | stones.in_catalog | v1 | Free visibility. |
| S6 Demand signal | "Buyers on Tray are looking at unheated spinel under 2 ct more than any other category this month." | anonymized market_facts | v2 | Tells a dealer what to stock. Requires ≥ 5 dealers and ≥ 10 events per category. |
| S7 Selling fast | "Padparadscha 1 to 3 ct sells in 21 days on average across Tray. Yours: 64." | market_facts | v2 | A benchmark that points to price or photos. |
| S8 Reference price | "Similar unheated Ceylon blue 4 ct: guide range $X to $Y." | licensed price guide | v3 | Reprice with confidence. |
| S9 Auction context | "Recent auction results for Kashmir sapphire." | public auction results | v3 | Context for rare stones; not a price. |
Cut on purpose: generic tips ("take better photos"), gamification, streaks, anything that sounds clever but has no button.
v3 sources: what is licensable, scrapeable, not
- Price guides. GemGuide sells subscriptions by user count and an API for pricing data, restricted to the trade with proof of affiliation (their site, 2026). That is licensable, and the right route, but expect a license fee and redistribution limits; do not assume we can show their numbers to third parties. Gemval: I could not find official terms; a forum post describes it as a subscription tool; treat it as "ask them".
- Auction results. Christie's, Sotheby's and Bonhams publish results publicly, but their terms generally restrict scraping and commercial reuse. Use hand-curated references with links, or negotiate; do not build a scraper.
- Fair calendar. Public dates, hand-curated, low risk (and you already have one).
- Lab turnaround times. Published on lab sites in places; can be curated by hand or confirmed by phone. Low risk, low value.
- Legal review before any external data appears in product.
9. Design references
| Reference | What I take | What I reject |
|---|---|---|
| The simplicity bar. Share flow, contact picker, chat-style history per client (consignments and sales as a timeline). | Its lack of structure. | |
| Carousell, Vinted | Camera first, form after, defaults that do the work. | Marketplace chrome, seller ratings. |
| Square POS (mobile) | Big buttons, one action per screen, plain words. | Retail-register metaphors (cart, checkout). |
| Shopify mobile home | The shape: numbers, attention, tips, each tappable. | Its density and charts; we simplify to 6 figures. |
| Apple Photos | Grid-first browsing, pinch density, instant search. | Albums and years; we sort by status and recency. |
| Revolut | Large tap targets, one field per step when it matters, custom keypad for numbers. | Gamified finance features. |
| Instagram (share card) | Image first, short caption. | Filters, social counters. |
| 1stDibs | Big imagery, calm layout, specs as a quiet table. | Auction-house formality and heavy navigation. |
| RapNet app (counter) | Nothing; this is what to avoid: desk tool shrunk onto a phone. | All of it. |
10. Design system
10.1 Look
Neutral gallery. Near-white and near-black surfaces, no brand color. The stones are the only saturated pixels on screen. The accent is the inverse neutral (black on light, near-white on dark), so primary buttons are plain and strong. Status colors are small and always paired with an icon or shape, so they work for the colour-blind and in sunlight.
10.2 Color tokens
All text-use colors below pass 4.5:1 on both surfaces (measured, ratios in the last column are against surface).
| Token | Light | Dark | Contrast on surface (L / D) |
|---|---|---|---|
surface | #FAFAF9 | #0E0E0F | - |
surface-elevated | #FFFFFF | #1A1A1C | - |
text-primary | #141414 | #F2F2F0 | 17.6 / 17.2 |
text-secondary | #5C5C5C | #A3A3A0 | 6.4 / 7.6 |
border | #D9D8D4 | #38383B | decorative |
accent (fill) | #141414 | #F2F2F0 | - |
on-accent | #FFFFFF | #0E0E0F | - |
positive | #1F7A4D | #5CC592 | 5.1 / 9.1 |
warning | #9A5B00 | #E8AE4A | 5.2 / 9.7 |
danger | #B3261E | #FF8A80 | check at build |
status-available | #1F7A4D | #5CC592 | 5.1 / 9.1 |
status-reserved | #9A5B00 | #E8AE4A | 5.2 / 9.7 |
status-consignment | #3550C8 | #8CA3FF | 6.4 / 8.1 |
status-sold | #6B6775 | #A9A5B3 | 5.3 / 8.0 |
Status is shown as a chip with an icon (check, clock, hand-with-box, tag-with-line) and a word, never color alone.
10.3 Other tokens
| Group | Tokens |
|---|---|
| Spacing (8pt grid) | space-1 4 (half step, allowed only inside components), space-2 8, space-3 16, space-4 24, space-5 32, space-6 48, space-7 64 |
| Radius | radius-sm 8 (chips, inputs), radius-md 12 (cards), radius-lg 16 (sheets), radius-full (pills, avatars) |
| Elevation | elev-0 none (default; borders carry hierarchy), elev-1 0 1px 2px rgba(0,0,0,.08), elev-2 0 6px 20px rgba(0,0,0,.14) for sheets. In dark mode, elevation is expressed by a lighter surface-elevated, not shadow. |
| Motion | motion-fast 120 ms (taps, chip toggles), motion-base 200 ms (sheets, state changes), motion-slow 320 ms (screen transitions); ease-out for entering, ease-in for leaving; reduced-motion setting turns everything to 0 ms fades |
The same names are used as Flutter ThemeExtension fields and as CSS variables (--surface, --text-primary, --status-consignment, --space-3, --radius-md).
10.4 Typography
One typeface: Inter (variable), with tabular figures turned on for every number that is a price, weight or count (font-feature-settings: "tnum" on web, FontFeature.tabularFigures() in Flutter). Thai uses Noto Sans Thai as the fallback, since Inter has no Thai glyphs; the line height is raised to 1.5 for Thai.
| Style | Size / weight | Use |
|---|---|---|
number-xl | 32 / 600 | Dashboard headline figure |
number-lg | 24 / 600 | Carat, price on stone detail |
title | 20 / 600 | Screen titles, section headers |
body | 16 / 400 | Everything readable (minimum) |
body-strong | 16 / 600 | Labels, buttons |
caption | 14 / 500 | Metadata only (timestamps, refs); never the only carrier of meaning |
10.5 Components and mapping
| Component | Flutter (Material 3 base) | Web catalog | Notes |
|---|---|---|---|
| Stone card (grid, list) | custom StoneCard on Card + Image | same markup | Square cover, status chip bottom-left, carat and price underneath. List variant adds specs row. |
| Status chip | Chip / InputChip themed | <span class="chip"> | Icon + word + status color; never color-only. |
| Filter chip, filter sheet | FilterChip in showModalBottomSheet | chips + dialog | Same attribute set app and web. |
| Bottom-sheet form | DraggableScrollableSheet | modal | Add stone, sale record, access sheet. |
| Stepper (carat, price) | custom keypad on TextField | numeric input + steppers | 48 px targets; decimal-first keypad. |
| Photo capture, gallery | camera plugin, PageView + InteractiveViewer | <img> carousel, pinch zoom | Square crop default, original kept. |
| Multi-select stone picker | GridView with selection state | n/a | Running totals bar. |
| Client row with access state | ListTile + status chip | n/a | States: not invited, invited, active, revoked. |
| Consignment card | Card with due-date chip | n/a | Per-stone outcome rows below: out, returned, sold. |
| Consignment slip (shareable) | rendered widget to image | public slip page | Same layout in both. |
| Share card preview | widget captured to PNG | OG image generator | The marketing artefact. |
| Catalog access sheet | bottom sheet | n/a | Toggle + Send on WhatsApp. |
| Dashboard stat tile | custom StatTile | n/a | Number-lg + label + trend. |
| Attention item row | ListTile with trailing action | n/a | One tap = resolve. |
| Suggestion card | Card | n/a | Text, "why", one button, dismiss. |
| Primary / secondary buttons | FilledButton / OutlinedButton, 56 px high | <button> | One primary per screen. |
| Empty state | custom | same | One icon, one sentence, one button. |
| Toast | SnackBar | toast | With Undo where possible. |
Web catalog reuses: stone card, filter chips, stone detail layout, status chip, and photo gallery, all from the same token file.
10.6 Iconography and photo treatment
- Icon set: one outlined set at 1.75 px weight (Material Symbols Outlined, rounded 24 px, as base) plus a custom stone-shape set of 14 shapes (oval, cushion, pear, round, octagon, emerald cut, square, heart, marquise, trillion, radiant, cabochon, sugarloaf, free-form) drawn on the same grid.
- Photos: square 1:1 crop by default on grids, original kept untouched; tap for full-screen with pinch zoom; videos play inline, muted, loop, 10 s cap; background neutral. Identical in app and catalog.
10.7 Seven key screens
Drawn in tray-screens.html: home dashboard, stock grid, stone detail, add stone, consignment detail with slip, share card, catalog grid.
11. Tech stack and architecture
Overview: Flutter app (SQLite on the device) <-> Supabase (Postgres, Auth, Storage hooks, Edge Functions) <-> object storage and image/video delivery on Cloudflare <-> Astro web catalog.
- Database and auth: the existing GemBridge Supabase project. Decided 13 October. Tray's tables go into their own Postgres schema (
tray.*) inside that project, next to the GemBridge tables, so the two products are easy to tell apart in the dashboard and in backups, and a mistake in one set of policies cannot silently open the other. Three user types in oneauth.users: dealers (atray.dealersrow, linked tovendors), client identities (tray.client_identities), and GemBridge's existing buyers, which Tray never touches. Role is set inapp_metadataby the server only; the client can't set it. The GemBridge bot's service-role key must not be the key the Flutter app ships with; the app uses the anon key plus the user's session, like any Supabase client. - How the two products stay apart inside one project. GemBridge code reads toolkit data through exactly one view,
tray.network_stones, which exposes only stones withshare_with_network = trueand only the public columns (no cost, no client, no sales). Everything else intray.*is invisible to the bot and the admin dashboard by policy. The existing GemBridge tables keep their current policies untouched; nothing in this plan needs them to change. - Row-level security (the heart of it). Rules in words:
- Dealers read and write rows where
dealer_idmatches theirs, and nothing else. - Anonymous visitors can read only: stones where
in_catalogis true, status is available (or reserved if the dealer chose), and dealercatalog_modeis public; and only the public columns, via a view without price unless the dealer shows prices publicly. - A client identity can read stones for a dealer only if a
client_accessrow withstatus = activeexists for that identity and dealer; prices fromstone_pricingfollow the same check. stone_privateis dealer-only, in its own table, with no view exposing it.- Test suite: a set of automated checks, run on every change, logging in as anonymous, as a client of dealer A, as a dealer B, and as the GemBridge bot's service account, and asserting each cannot read cost, other dealers' stones, revoked access, or anything outside
network_stones. This GemBridge database had a live leak from an "allow all" policy in September 2026; sharing the project means that class of mistake would now expose dealers' cost prices, so these tests are not optional and run before every deploy. - Media: originals in Cloudflare R2 (no egress fees; a video-heavy catalog on a per-GB egress bill is where margins die), served through Cloudflare image resizing as 400 / 800 / 1600 px WebP with a blurhash placeholder. Videos are transcoded once to 720p H.264 MP4 plus a poster frame (Cloudflare Stream if cost allows; otherwise an Edge Function with ffmpeg). Stones not in the catalog use a private bucket with short-lived signed URLs; catalog media is public by URL with unguessable keys.
- Compression on device: photos resized to 2048 px long side, quality 82, before upload; the full-res original uploads later on Wi-Fi only. Videos trimmed to 15 s, compressed on device to ~720p.
- Offline-first: the app's source of truth is a local SQLite database (Drift). Every write goes to SQLite and an outbox table; a background worker pushes the outbox when connected. Media uploads are resumable (tus protocol), queued, and survive app restarts. The stone is usable (share card from local files, memo, sale) before any upload finishes. Evaluate PowerSync (Postgres to SQLite sync with a Flutter SDK) in a spike at the start of phase 2; otherwise hand-roll the outbox for the prototype.
- Conflicts (two devices edit the same stone offline): attribute edits merge per field with last-writer-wins by server timestamp; changes that move stock (memo, sale, reserve, split) are events and are never overwritten, only appended. If two events are incompatible (the same stone sold and also on a memo), the server accepts both, flags the stone, and raises a "Conflict" item in Needs attention with both stories on one screen. v1 has one device per dealer in practice, so this is rare; the design just must not lose data.
- Dashboard metrics: computed on the device from local tables (queries over a few thousand rows, milliseconds). A nightly Supabase cron writes
dealer_metrics_dailyfor trends and for the cross-dealer pipeline; the app caches the last good values so the dashboard opens instantly with no signal. Catalog-view figures are the one thing that need the server and arrive at the next sync. - Web catalog: Astro on Cloudflare (SSR for public pages, islands for filters and sign-in). Chosen over Next.js because pages are mostly read-only, photo-led and need to be tiny and fast on 4G. Public pages are server-rendered with proper Open Graph tags so WhatsApp link previews show the stone photo and the specs. HTML is cached at the edge and never contains prices: prices are fetched after login by an authenticated API call. A cached public page can therefore never leak a price. Next.js remains a fallback if a team preference emerges.
- Auth model for clients (no public signup):
- Dealer grants access; server creates an invite with a single-use, 24-hour token and returns a link.
- The dealer's WhatsApp message carries the link. Opening it exchanges the token for a session (180 days, sliding, stored in a secure cookie).
- Everyday path: the client opens any catalog link from the chat; their session is already there. No password, no code.
- Session expired / new phone: the client taps "Send me a new link"; the dealer receives a one-tap "Mr. X asks for a new link" item and re-sends from WhatsApp. Costs nothing, keeps the dealer in control, avoids WhatsApp Business template fees, and is still seconds of effort.
- Optional: if the client gave an email, a standard emailed magic link works without the dealer. A password can be set from the first-login prompt for those who want one.
- Sessions are per device, listed to the dealer, revocable. More than 3 simultaneous devices on one client flags the account.
- Other services: Edge Functions for token issuing, slip snapshots and OG images; a scheduled job for overdue statuses and daily metrics; Sentry for crashes. Costs at 100 dealers are small: Supabase Pro plan, R2 storage pennies per GB, Apple/Google developer accounts. No per-seat vendor fees.
11b. Do we need a model that predicts a stone's characteristics?
April asked this on 13 October. Short answer: yes, but as an assistant that fills the form, never as an authority that describes the stone. And not a model we train ourselves at first.
What a phone photo can and cannot tell you. Be honest about this before promising anything to dealers:
| Attribute | From a phone photo | How we'd get it instead |
|---|---|---|
| Shape | Reliable | Vision model suggests, dealer confirms with one tap |
| Species / variety | Plausible guess (red oval on a tray: ruby, spinel, garnet or glass). Useful as a pre-selected chip, dangerous as a fact | Suggest the top 2, dealer picks |
| Colour description | Rough. Phone white balance and the tray change everything | Suggest a trade term, dealer edits |
| Carat, dimensions | Impossible from a photo | Keypad, or a Bluetooth scale later |
| Clarity | Weak | Dealer picks from 4 chips |
| Origin, treatment | Impossible from a photo. Only a lab can say this, and even labs disagree | Read it off the certificate, or the dealer says it |
| Price | Not from the photo. From the dealer's own history and, later, cross-dealer data | Section 8, S1/S7/S8 |
Where a model earns its place, in order of value
- Certificate reading (phase 2). Dealers photograph the GIA/GRS/Gübelin/AIGS/GIT report that is already in the parcel. A vision-language model reads lab, report number, weight, dimensions, species, origin, treatment and colour comment, fills the form, and the dealer confirms. This is the single biggest typing saver, and the extracted values are the ones that matter for price. Start with Claude's vision through the API; reports are printed documents and this works well today. Store the extracted fields next to the scan, and link the report number to the lab's verification page so the buyer can check it themselves.
- Natural-language search (prototype or phase 2). "unheated ceylon blue 3 to 5" → filters. A small prompt to a language model, or a rules parser for the trade vocabulary we already control. Cheap, high daily value.
- Photo-to-form suggestions (phase 2). Shape and a species guess, pre-selected as chips. The dealer keeps the last word, always, and the UI never says "detected ruby"; it says "Ruby?" with a tap to change. Wrong guesses cost one tap; wrong facts cost trust.
- Wishlist matching (phase 2). Structured match first (species, size band, treatment, origin). A model only for the free-text wishes dealers type ("something special for a man's ring, blue, under 10k").
- Pricing help (v2/v3). Not a model that "predicts value". A reference range from the dealer's own sell-through, then anonymised cross-dealer data, then licensed guides, each labelled with where it came from. A number with no source is a number a dealer will ignore or, worse, trust.
Why not train our own model now. We have no labelled photos: GemBridge holds 418 stones and, as of the October audit, zero photos. Tray fixes that: every stone added is a photo with dealer-confirmed labels (species, shape, colour term, treatment, origin, lab, price, and whether it sold). After a few thousand stones across dealers that is a dataset nobody else has, and the moment to consider fine-tuning a vision model on it, or selling the insight back as market intelligence (section 8.3, v2). Until then, a hosted vision model plus the dealer's thumb is cheaper, better and faster to ship.
Guardrails, because dealers' reputations ride on descriptions
- Every model-filled field shows a small "suggested" mark until the dealer taps it. Unconfirmed fields never reach the catalog or a share card.
- Origin and treatment are only ever filled from a certificate or by the dealer. The app never guesses them.
- Model calls run in the background after the stone is saved; adding a stone never waits for a model.
- Every suggestion, acceptance and correction is logged. That log is how we measure whether the model helps and, later, what we train on.
- Cost: a few cents per stone for vision calls, so under $1 per dealer per month at typical volumes. Fine inside the Pro price; cap it on Free.
12. Build phases and milestones
I build; you steer. Durations are my estimate of calendar time with you testing weekly; they move if the prototype test says "change it".
| Phase | What | Weeks | Done when |
|---|---|---|---|
| 0. Prepare | Count doors at JTC. Interview 8 dealers about their last lost sale and last memo. Pick 5 design partners. Draft tray-screens as clickable tap-throughs. | 1 to 2 | 5 dealers agree to a test; a real sheet from each. |
| 1. Prototype | Section 3 scope. TestFlight and APK to the 5. Concierge import. Public catalog (no logins). | 4 to 5 | 5 dealers: 30 stones each in the app, at least 3 real memos created, at least 10 real share cards sent, in a 14-day test. Decision: keep going or change. |
| 2. Core v1 | Sync and backup, in-app Excel import, client logins and view tracking, wishlists and matching, reserved/attention rules, price controls, top-clients metric. | 8 to 10 | 12 dealers live. 4 client logins per dealer. The full catalog loop works end to end. |
| 3. Paid launch | Thai UI, payments (PromptPay and card), onboarding polish, slip receipt confirmation, QR parcel scan. | 4 | 20 dealers; first paying; weekly active target met. |
| 4. Intelligence | Cross-dealer market facts (v2 suggestions), fair calendar, performance polish. | later | 100+ dealers and consent terms in place. |
| 5. Reference data | Licensed price guides, curated auction context. | later | License signed. |
Later versions, one paragraph each.
- Desktop admin. Bulk edit, reporting and exports for bigger shops and bookkeepers. Not needed until shops have 1,000+ stones or a second person.
- Per-client stone selection and price tiers. Show client A the wholesale price and client B the retail; hide selected stones from selected clients. Built on the single catalog read policy.
- Signed slips. The receiver confirms by tapping the slip link (and later signs on screen), producing a timestamped acknowledgement; the most valuable dispute tool.
- Multi-user shops. Roles (owner, staff) with cost price hidden from staff; this is when row-level rules get a second axis.
- LINE and WeChat sharing. Same card, different share targets; essential for Chinese buyers; formats differ and need testing.
- Custom domains.
stones.theirshop.compointing at the same catalog; a Pro Plus perk. - Cross-dealer market intelligence. Anonymized buckets, minimum group sizes, opt-in.
- Marketplace across dealers. Only if dealers ask for it; it changes the relationship and risks the trust that the product is built on.
13. Risks and open questions
| Risk | What could go wrong | What I'd do |
|---|---|---|
| Trust | Dealers will not put stock, cost prices and client lists in someone's cloud. Sharper now: that cloud belongs to a company that also talks to their buyers. A dealer who learns GemBridge can see his cost price or his client list deletes the app and tells the floor. | Phone-first (data stays on the phone until they press publish), cost price in its own private table, an export button on day one, a plain-language privacy page, no sale of identifiable data, ever. Be physically present in the building. Plus, because of the shared backend: sharing with the GemBridge network is per stone, off by default, and visibly so in the app; and the policy tests in section 11 are what make that promise true. The umbrella brand should be announced only once the toolkit stands on its own; "GemBridge wants to see my stock" is the wrong first impression. |
| Client credential leakage and screenshots | A client forwards the link or screenshots prices to a competitor. | We cannot stop a screenshot. Mitigate: watermark prices with the viewer's first name (dealer toggle), per-client session cap, access log, one-tap revoke, price-tier later. Culturally, the trade already accepts this risk on WhatsApp. |
| Multi-currency | THB and USD both everywhere; the same stone sold in two currencies. | Store each price in the currency it was entered, plus an optional dealer-set rate; dashboards show a single display currency with the rate used; never silently convert historical sales. |
| Thai and English | English-only loses Thai dealers. Spelling and units differ. | Strings externalised from the first commit; Thai in phase 3; photo and icon-led screens reduce the load either way. |
| Wrong or obvious suggestions | The suggestion block teaches dealers to ignore Home. | Few rules, shown only with enough data, "why" on every one, dismiss with feedback, suggestion_log to kill rules nobody acts on. |
| Memo disputes | "That stone was not on the slip." | Frozen snapshot with a hash and public link, per-line outcomes with timestamps, and receiver confirmation in phase 3. Not legal advice. |
| Offline double-edits | Two devices change one stone. | Event log, per-field merge, conflict item on Home (section 11). |
| Dealers lack time to enter data | The first week is typing, and they stop. | Concierge import, camera-first, defaults, 30-stone starting goal, the free tier works with 5 stones. |
| WhatsApp policy / template costs | Meta restricts automated messages (your own number was blocked from some countries before). | Everything sends from the dealer's own WhatsApp via share, not from a business API. No automated outbound messaging in v1. |
| Scope creep | Eight features plus a web app is too much. | The prototype list is firm; every feature must pass "will a dealer touch it weekly?" |
Open questions (none block the prototype; ask in this order)
- Which 5 dealers do you trust to be design partners, and will they let us use their catalog as a demo? The GemBridge dealers with inventory on file are my first candidates.
- Do any dealers already use GemCloud or similar? One user interview would change the competitor table more than any web search.
- What does a Thai dealer say when asked: "Would you let your clients see your stock, and your prices?" This sets the catalog default.
- When does the umbrella brand go public? My recommendation: not before 20 dealers use the toolkit weekly.
- Who pays the first costs (developer accounts, media storage)? Supabase is already paid for; the new line items are small, but they need an owner.
Decided: Tray shares the GemBridge Supabase project and auth, and will sit under one umbrella brand later (April, 13 October 2026).