TBBN.Merchant Platform docs
Businessdocs/business/conflict-resolution-log.md

Conflict / Contradiction / Ambiguity Resolution Log

Source material (ProjectB.docx, TBBN_Flow.docx) partially overlapped and partially contradicted itself, as expected from two passes of brainstorming on the same idea. Every resolution below is now the canonical answer for this repo — do not re-litigate without updating this log and everywhere it's referenced.

#Conflict / ambiguityResolutionStatus
1One doc proposes a "Global Liquidity Fee" ($1-5 or 0.25%-1% per cross-merchant trade); the core principle elsewhere is "TBBN never handles money," and the user's explicit instruction limits TBBN payments to merchant-plan billing and seller boosts only.Cut the liquidity fee entirely. Taking a cut of trade value makes TBBN a payment intermediary / possible money-transmitter, contradicting the core principle. Replaced with a 4x metering multiplier on cross-merchant API events, billed on the merchant's normal SaaS invoice — same "merchant pays for plan" bucket, zero touch of trade proceeds.Resolved
2Two different Trade Session state-machine diagrams appear (RESERVING_ITEMS/ITEMS_RESERVED/FULFILLMENT_IN_PROGRESS in one; RESERVING/RESERVED/FULFILLMENT in the other), and one shows sequential Payment A → Payment B, contradicting the explicit "B does not wait for A" correction elsewhere in the same source.Canonical state machine (see docs/architecture/trade-session-state-machine.md): DRAFT → PENDING → OFFERED → ACCEPTED → RESERVING → RESERVED → WAITING_CHECKOUT_A + WAITING_CHECKOUT_B (explicitly parallel) → CHECKOUT_A_COMPLETE / CHECKOUT_B_COMPLETE (independent) → LOCKED → FULFILLMENT → FULFILLED → COMPLETED, plus failure states CANCELLED, EXPIRED, PAYMENT_FAILED, RESERVATION_FAILED, FULFILLMENT_FAILED, RETURNED, REFUNDED.Resolved
3"Initiator" and "requester" read as if they might be persistent roles (used throughout the flow narrative), but the source explicitly states they're not meant to be stored, and that once an offer converts into a trade session both sides act symmetrically. Meanwhile the trade-session schema needs some deterministic column assignment (A vs B).Initiator/requester remain documentation-only flow labels, never a DB column. On offer→session conversion, the existing listing's owner (offer target) is stored in the A slot and the offer proposer in the B slot — a storage convention only, not a workflow rule; business logic (parallel checkout, symmetric actions) must never depend on which slot a seller is in.Resolved
4Merchant tiers are named inconsistently: one place says just GLOBAL / PRIVATE; another defines four capability tiers GLOBAL / PRIVATE / PREMIUM / ENTERPRISE; a separate revenue-model section names SaaS tiers Starter / Growth / Enterprise with no stated mapping to the capability tiers.Single unified ladder, 1:1 with SaaS pricing: Sandbox → Starter (Global only) → Growth (Premium: Global+Private+Both, widgets, white-label) → Enterprise (+ Enterprise-only visibility, dedicated infra). See pricing.md.Resolved
5"No individual signup on our platform directly" is stated as an absolute rule, but the same source also describes sellers logging into TBBN directly to manage their account and pay for boosts, and describes issuing a login token to email/phone for sellers who signed up through a global merchant.A seller never fills out a signup/registration form on TBBN. Their identity is always created by a merchant's verification call. Once federated (2+ linked merchant accounts, matched via sequential email-then-phone OTP), a seller may authenticate into TBBN Exchange — this is login to an existing identity, not signup, and does not violate the no-direct-signup rule. See glossary.md.Resolved
6Multi-item trading is described in the "current" 1↔1 flow narrative, but the improvement notes and the final schema examples (offeredListings array, listingsA/listingsB arrays) assume N↔M from the start; unclear which is v1.Modeled N↔M from day one in the schema (offer_listings_a/b, trade_session_listings_a/b join tables) — retrofitting multi-item support onto a 1:1 schema later is far more expensive than modeling the array relationship now, and the value-comparison logic doesn't get materially harder for N items vs 1. Ring/multi-hop trades (a separate, harder capability) remain roadmap-only (Phase 21).Resolved
7The plain-English listing flow implies a seller enters either a requested amount or a willingness to pay, never both ("if they enter an amount on the Request amount space... if they enter any amount on the willing to pay section..."), but the general TradeValue = OriginalPrice + RequestedAmount − WillingToPay formula treats both fields as independently present.requested_amount and willing_to_pay are mutually exclusive per listing (validation-layer constraint); the formula is still written generally because it's evaluated with the unset field defaulted to 0, which happens to make the general formula collapse correctly to the single-field cases in the narrative.Resolved
8It's implied but never explicit whether both trade sides can check out in any order, or whether the party who receives the higher-value item must go first, etc.Confirmed explicit in source ("payment order/sequence... is not really relevant"): either side may complete checkout first, in any order, immediately after the session reaches RESERVED. Fulfillment starts only once both sides report *_COMPLETE. This is called out as a common misconception worth a dedicated warning in trade-session-state-machine.md.Resolved
9Not addressed anywhere in source material: does a seller's paid boost affect the matching algorithm (Trade Compatibility Score) or only where a listing appears in browse/search results?Superseded by item 14 — boosts were removed entirely, so this question no longer applies. Kept here for history: the recommended default while boosts existed was display/ranking only, never the Trade Compatibility Score, to keep monetization separate from match quality.Moot (see item 14)
10aNot addressed in source material: TBBN directly charges individual consumers (sellers) for boosts. This makes TBBN merchant-of-record for that specific flow, with tax/refund/KYC obligations distinct from the merchant-SaaS-billing side.Resolved by removal — see item 14. The founder/legal question this row flagged is now moot because the flow it concerned no longer exists; docs/legal-flags/open-legal-questions.md item 1 has been updated accordingly.Resolved (by removal)
10bNot addressed in source material: seller identity federation deliberately links one person's data across otherwise-independent merchant businesses — a cross-controller data-linking pattern with GDPR-class implications once EU merchants onboard, plus unclear which merchant's jurisdiction governs a federated record spanning e.g. an EU and a US merchant.Not resolved here — flagged for the founder/counsel. See docs/legal-flags/open-legal-questions.md. Safe to defer past Phase 0-3, must be resolved before EU merchant go-to-market.Open — founder/legal decision
11Listing "Note" free-text moderation is a hard requirement (block phone/email/address/contact-info), but source material doesn't say what happens on a false positive (e.g. "size 10.5" flagged as phone-like).moderation-service returns a structured rejection reason to the merchant's listing-sync call (never a silent drop); merchant UI surfaces the reason; TBBN ops console (Phase 19) gets a manual-override/appeal queue. Non-legal, UX-only resolution.Resolved
12Trade session expiry/timeout values are referenced ("a couple of days") but never given a precise number anywhere in source material.Default: 72 hours to complete checkout after RESERVED, 24 hours from OFFERED to ACCEPTED before auto-expiry; configurable per merchant (merchant_settings) at Growth tier and above. Documented as a tunable default, not a hard business rule.Resolved (default, revisitable)
13Source material lists api-key-service and rbac-service as standalone services in the target architecture diagram, but Phase 0-3 doesn't have enough independent load/team size to justify a network hop between them and merchant-service yet.Both are scaffolded as separate folders (visible in the target architecture) but their working logic in this pass lives inside merchant-service/auth-service; each stub's README documents this and the extraction trigger (team size or load, not a fixed phase number).Resolved
14Post-launch business decision (not from source material): the founder instructed (2026-07-02) that TBBN should have exactly one payment flow — merchants paying for their plan — and that seller listing-boost payments should be removed entirely, "Just businesses."Removed the seller-boost payment flow completely. Deleted services/boost-payment-service; removed BoostPurchase/BoostTier from packages/db-schema's Prisma schema; renamed packages/openapi-spec/spec/boosts.yaml to billing.yaml and dropped its /v1/boosts paths, keeping only the merchant-billing paths; removed the boost.purchased event from packages/event-contracts; removed all boost UI from apps/exchange-web (account page, pricing copy) and apps/marketing-site (pricing page) and the boost_purchases feature flag from apps/ops-console. docs/business/pricing.md, business-model.md, and README.md updated to describe one payment flow, not two. This also resolves items 9 and 10a above by elimination, and removes the merchant-of-record legal exposure item 10a flagged (see docs/legal-flags/open-legal-questions.md).Resolved
15Post-launch clarification (not from source material): the founder specified (2026-07-02) that merchants should only ever be required to send a seller's phone number, email, and a name/nickname/username — whichever they can legally share, not a mandated legal name. The SellerVerifyInput.name field already existed but its semantics were ambiguous (read as if a legal name were expected) and, separately, the value was accepted by seller-identity-service but never actually persisted to the Seller row.Documented name everywhere it appears (OpenAPI spec, @tbbn/types, the service DTO, the Prisma schema) as accepting a name, nickname, or username, at the merchant's legal discretion — never assumed to be a legal name, never required to be verified as one. Added a name column to the Seller model (previously missing) and updated seller-identity-service's verify()/maybeFederate() to actually store and reuse it. No additional identity fields are requested from merchants beyond phone, email, and this one flexible name field.Resolved
16Founder proposal (2026-07-04): lower Starter/Growth flat SaaS prices, add an annual-billing discount option, and charge a 1% of a listing's original price fee per checkout so "TBBN only makes money when the business makes money." A 1%-of-trade-value fee is structurally the same "Global Liquidity Fee" mechanism already cut in item 1 — a fee that scales with a specific transaction's dollar value is a take-rate (payment-processor/marketplace shape), not a usage fee, regardless of whether the money physically routes through TBBN, and it reopens the money-transmitter/marketplace-positioning risk docs/legal-flags/open-legal-questions.md already flags for counsel review.Kept the "no percentage of trade value" principle, adopted everything else. Starter dropped $299→$200/mo ($150/mo annual, 25% off), Growth dropped $999→$500/mo ($400/mo annual, 20% off), Enterprise moved from a $5,000 floor to "Contact us" (custom quote, capabilities still listed). The checkout-orchestration fee is maintained as a flat $1.50 per completed checkout (not a percentage) — same take-rate-avoidance logic as item 1, formalized in pricing.md's "Why a flat per-checkout fee, not a percentage" section — with the existing 4x cross-merchant multiplier still applying (3x floor at Enterprise). Merchants may pass this flat fee through to their own seller/trader on their own checkout; this is the merchant's commercial decision on the merchant's own checkout, not a new TBBN-to-seller payment flow. Also formalized: monthly-billing merchants are invoiced plan-fee + usage together every cycle; annual-billing merchants prepay the plan fee once/year but still get invoiced for usage/checkout fees monthly; a 7-day grace period applies past any invoice's due date, after which production API access is suspended until paid in full. The grace-period countdown and automatic suspension are specified but not yet implemented in services/billing-service — see that service's README. 2026-07-05 correction: checkout-service's original implementation didn't match this policy — it recorded the CHECKOUT_ORCHESTRATION usage event at checkout open (deduplicated per unique merchant across both sides), not at checkout completion per side. That meant a same-merchant trade billed $1.50 once instead of $3.00 (once per side), and a checkout that opened but never completed was still billed. Moved the usage-event recording from openCheckout() to the payment.completed branch of handlePaymentWebhook(), keyed per side — see services/checkout-service/README.md.Resolved
17Founder instruction (2026-09-09): every "TBBN never handles money/escrow/taxes/fulfillment" and mirrored "Merchants always handle it" statement across the repo was stated as an absolute, permanent claim. The founder pointed out that TBBN Rev.5 already introduces a real, narrow exception — TBBN Space (see ProjectB.Rev5.docx / TBBN_Flow.Rev5.docx §6-7): individuals pay TBBN directly, on TBBN's own site, for a chosen Space host's meetup/drop-off/pickup service, and TBBN pays the host out and keeps a variable, TBBN-set commission. An absolute "never" is therefore already inaccurate the moment TBBN Space monetization ships, and would require publicly walking the claim back. The founder's instruction: "if we ever start processing payments for our listings, we will not have to do so much explaining."Reworded every such claim from "never"/"always" to "does not currently handle"/"currently handle," repo-wide — CLAUDE.md, README.md, docs/business/business-model.md, pricing.md, glossary.md, docs/architecture/system-overview.md, service-catalog.md, packages/db-schema/prisma/schema.prisma, packages/openapi-spec/spec/openapi.yaml, packages/ui/src/SiteFooter.tsx, services/billing-service/README.md, and the apps/marketing-site homepage/company/pricing/solutions/how-it-works pages. The underlying behavior is unchanged — this is a wording hedge only, scoped strictly to claims about TBBN not handling money/escrow/taxes/commissions/shipping/customs/duties/fulfillment/returns/disputes/refunds/merchant-onboarding/seller-KYC/merchant-of-record, and the mirrored merchant-handles-it claims. Unrelated "never" statements (e.g. "TBBN never originates a listing," sandbox-isolation "never touches production data/another developer's sandbox") were deliberately left untouched — they aren't money claims and don't need the hedge. TBBN Space's actual payout/commission mechanics are the concrete, real reason this hedge exists — see TBBN_Flow.Rev5.docx §7 for the full flow (free onboarding, host-set per-service pricing, payment collected on TBBN's own site before host notification, TBBN-managed payout, TBBN-set commission).Resolved
18Founder instruction (2026-09): a full Business Profile system (business.tbbnetwork.com) — a business entity with multiple physical Branches, owned by a user who can own multiple businesses, with Merchant (and later Space) products required to link to a Business rather than floating free. A necessary consequence: "all registered users will automatically have a marketplace profile," i.e. direct self-registration. This directly reverses item 5 above and the matching passages in glossary.md/business-model.md, which stated as a resolved rule that "a seller never fills out a signup/registration form on TBBN... their identity is always created by a merchant's verification call."Reversed, not routed around. Direct self-registration (POST /v1/auth/member/register + /member/otp/verify on auth-service) is now the primary way anyone gets a Marketplace profile (Seller, with a User created together at registration — never conditionally after a federation threshold). seller-identity-service's merchant-verification + sequential email/phone OTP flow (item 5's original mechanism) is kept, but repurposed: it now attributes a merchant's fed seller-ref to an already-existing (or newly-created-on-the-spot) Marketplace account, rather than being the only way an individual identity can come into existence. maybeFederate()'s 2-link creation-threshold gate is removed entirely — "federated" is now a derived display fact (2+ merchant links), not an identity-creation gate. Also added: Business/Branch models and a many-to-many BusinessMerchantLink join (new business-service, port 3036, mirroring Google's Merchant-Center-links-to-Business-Profile pattern — independently-owned/linked accounts, not one merged model), a new apps/business-dashboard (business.tbbnetwork.com, private management dashboard only, not a public profile site), and a businessId now required on merchant-service's onboarding. See docs/architecture/db-erd.md "Domain: Business" and docs/phase-plan/roadmap.md Phase 22. TBBN Space as an independent bookable product with real payments (Phase 23), a Ratings & Reviews system (Phase 24), and consolidated Business-level billing (Phase 25) are follow-on phases, deliberately not built in this pass.Resolved
19Founder instruction, grounded in TBBN_Flow.docx §7 / ProjectB.docx §6/§9.3: build TBBN Space as a real product — free host onboarding, host-set per-service pricing (meetup facilitation, drop-off/pickup custody with a host-set hold-duration policy), payment collected on TBBN's own site (the host is not notified of a booking until payment clears), TBBN payout to the host net of a TBBN-set commission that can vary host to host, with payout deferred until the exchange is confirmed to have happened (not released at payment-clearing time). A codebase audit while designing this surfaced two real, previously undiscovered gaps: (a) trade-session-service has never implemented the individual/peer-to-peer trade-completion state machine the source docs describe (§8.2/§7.2) — every existing state/code path assumes a Merchant checkout; (b) Listing.merchantId is a required, non-nullable field, meaning individual (non-Merchant) listings — the thing a peer-to-peer trade and a "pick a Space at listing time" flow would attach to — don't exist in code at all. Building both, on top of Space itself, would be substantially larger than this pass and is a separate, pre-existing gap this work did not go looking for.TBBN Space shipped as a fully standalone, directly-bookable product (new space-service, port 3037, and apps/space-dashboard, space.tbbnetwork.com) — any Marketplace member can book any Space-enabled Branch for any reason, independent of any TBBN listing or trade session. SpaceBooking.tradeSessionId is a nullable forward-compatible hook only; no code path sets it. The two gaps above are flagged, not fixed — see docs/phase-plan/roadmap.md Phase 26 ("Individual Listings + Peer-to-Peer Trade Completion"), which must land before Space can actually be picked at listing time in exchange-web. Money mechanics: a new StripeConnectPort/ConsoleStripeConnectAdapter (services/space-service/src/stripe/) — structurally the opposite direction from billing-service's StripePort (collect-then-payout, not recurring subscription collection only) — deliberately kept in its own service rather than folded into billing-service, per TBBN_Flow.docx §11's explicit warning against bolting a payout direction onto billing-service "as if it were the same thing." Liability (§7.5) and money-transmitter exposure are flagged, not answered — see docs/legal-flags/open-legal-questions.md item 3.Resolved (scoped)
20Founder instruction: "each aspect should have its own billing section... billed collectively and not based on individual profiles" — Merchant SaaS billing needed to move from being rooted at the Merchant to being rooted at the Business, since a Business can run multiple linked Merchant accounts (Phase 22) that should be billed as one. A schema audit confirmed MerchantSubscription/ApiKey/UsageEvent were all hard-rooted to Merchant with no businessId anywhere — this was a genuine relation change, not a rename.MerchantSubscription renamed and re-rooted to BusinessSubscription (one Stripe customer/subscription per Business, covering usage across every linked Merchant) — the same kind of clean rename Phase 22 already did for Seller.federatedUserIduserId, safe because no migration history or production data exists yet. ApiKey/UsageEvent stay Merchant-rooted (usage is genuinely per-API-key at the point it's emitted); billing-service's usage aggregation now resolves every Merchant linked to a Business (BusinessMerchantLink) and sums across all of them. Merchant.tier is kept (existing tier-gated capability checks elsewhere in the codebase read it directly) but is now synced across every Merchant linked to a Business whenever that Business's subscription tier changes — so it stays a valid read-through cache, not a second source of truth. A new GET /v1/billing/statements/:businessId returns merchantPlan (owed) and spaceEarnings (already paid out via space-service, net of commission) as two clearly separate, never-merged sections — satisfying "each aspect gets its own billing section" without conflating TBBN's two different money directions (collection vs. payout) into one number.Resolved
21Founder instruction: "Seller's on our merchants platforms can now complete a barter trade on our merchants platform without having an account with us as long as the merchant has the right premium/subscription type" — directly reverses part of Phase 22's "every Seller has a User from creation" rule (item 18).Seller.userId made nullable again (String? @unique, Postgres permits multiple NULLs in a unique column) — a "headless" Seller is a merchant-tracked trade participant with no linked TBBN account at all. Gated by a new denormalized Merchant.headlessSellersAllowed boolean, synced onto every linked Merchant the same way listingModeAllowed already is, by billing-service's syncMerchantTiers() — GROWTH/ENTERPRISE only (HEADLESS_ALLOWED_TIERS). New POST merchant/sellers/headless (seller-identity-service) creates one, with no email/phone matching attempted (there's no account to match against) — the 4 pre-existing Seller.create() call sites (sellers.service.ts::verify(), auth.service.ts::verifyMemberOtp(), sandbox-service) are all unaffected, since none of them omit userId. A peer-to-peer trade confirmation (Phase 26) for a headless Seller's side records confirmedByMerchantUserId instead of confirmedByUserId, since there's no User to record.Resolved
22Founder instruction: "Users can link their accounts through a secure auth handshake (signing into their merchant account); conduct a deep research on the best approach" — researched against Google's own "Account Linking with OAuth" (TBBN as IdP, Merchant as OAuth client) and Shopify's OIDC/OAuth2+PKCE customer-linking pattern, both converging on the OAuth 2.0 Authorization Code flow. This directly replaces seller-identity-service's existing sequential email-then-phone OTP cross-merchant linking flow (Phase 3) — a merchant merely knowing a seller's email/phone was never real consent to link, and OTP-via-merchant-relay doesn't scale to arbitrary merchants the way a real OAuth client/redirect flow does.New OAuthClient/OAuthAuthorizationCode models; linking/pending-link.store.ts and linking/link-otp.service.ts deleted entirely, along with the v1/sellers/link/otp/* routes. Client credential issuance (POST v1/oauth-clients, mirrors ApiKey's shape) lives in merchant-service, since it's the same credential-management domain that service already owns. Protocol endpoints (GET v1/link/oauth/clients/:clientId, POST v1/link/oauth/consent, POST v1/link/oauth/token) live in seller-identity-service, since a successful exchange only ever writes a SellerMerchantLink row — the domain that service already owns. The authorization code is Postgres-backed and hashed, OtpChallenge-shaped (not PendingLinkStore-shaped) — it's a security-critical bearer credential exchanged over an out-of-band redirect, not short-lived in-process coordination state. SellerLinkMethod.OTP_MATCH removed, replaced with OAUTH_AUTHORIZATION and HEADLESS_CREATED (item 21) — 6 files touched for the enum's full blast radius. Deliberately link-only: no access/refresh token is issued to the Merchant; a general-purpose OAuth Authorization Server is out of scope for this pass. The new consent screen lives at account.tbbnetwork.com/link/authorize (item 23), not on the Merchant's own site.Resolved (scoped)
23Founder instruction: "Basic account information, profile, and setting can all be handled under tbbnetwork.com domain. Also sign up and sign in can only be under that domain as well... account.tbbnetwork.com is to be used for payments, subscription management, linking/delinking of services... If a visitor lands on any of our subdomain, reads about the services... and clicks on sign up, they are to be taken to the main domain (tbbnetwork.com) signup page and are brought back to their intended page." This directly collides with exchange-web's documented domain (exchange.tbbnetwork.com) and marketing-site's current occupancy of root tbbnetwork.com.apps/exchange-web becomes the app serving root tbbnetwork.com, confirmed with the user ahead of implementation. Its hardcoded self-referential links (2× in app/page.tsx, 2× plus a text list in components/SiteFooter.tsx) were rewritten; a new app/register/page.tsx was added (the direct-self-registration backend — POST /v1/auth/member/register — already worked end-to-end since Phase 22; the missing piece was purely this UI). apps/marketing-site retired as a deployed app (code kept, undeployed) — its "Two ways in," TBBN Space explainer, and Safety-tips sections folded into exchange-web's home page as new local components (AudienceSplit/SpaceExplainer/SafetyTips); its pure-positioning copy was not ported, having no functional counterpart in a live app. New apps/account-dashboard (account.tbbnetwork.com, port 3800) has no login of its own — middleware.ts reads an httpOnly tbbn_refresh cookie (Domain=.tbbnetwork.com, refresh-token only, set by a new exchange-web Route Handler at login/registration) and refreshes it server-side, forwarding the result to Server Components as request headers; this has to be Middleware, not a plain Server Component, because refresh tokens rotate on use and only Middleware/Route Handlers can rewrite a response cookie. A new shared packages/utils/src/return-to.ts (isAllowedReturnTo) is the single sanitization point for the cross-subdomain ?return_to= pattern — allowlisting tbbnetwork.com/*.tbbnetwork.com only, closing the open-redirect risk the founder's own instruction flagged.Resolved
24Founder instruction: "On our marketplace, we only orchestrate checkouts when a user wants to trade a merchant product that has been opted in for barter. If a merchant is using our platform for a wider reach and is not interested in our barter trade, we send users/visitors to the item's detail page on the merchant platform like a google merchant product." No sale/trade/either flag existed anywhere on the actual Listing Prisma model — only an aspirational doc-comment on Merchant.tradingEnabled referencing a never-built Listing.trade_or_sale_flag.New Listing.listingType enum (SALE/TRADE/BOTH), implemented for real, applying uniformly to Merchant-fed and individual (Phase 26) listings alike — one field satisfies both this item and item 19's "opted in for a trade" language. The actual gate lives in offer-service::create(), earlier than checkout: a SALE-only listing can never have an Offer made against it, so it never reaches trade-session-service/checkout-service at all. exchange-web's listing-detail page renders a SALE listing without an OfferBuilder, showing a "View this item on [Merchant]'s site →" button (new Listing.merchantItemUrl, populated by the Merchant at sync time) instead — Google-Merchant-product-card behavior exactly: TBBN shows the card, the transaction happens off-platform. If no merchantItemUrl was supplied, a plain "not available for TBBN checkout" message is shown rather than guessing a URL.Resolved
25Founder instruction: "Users can also pick a preferred TBBN space (they will see TBBN spaces 200 miles of their radius along with the space's fees, ratings and reviews, amenities and services available in the space, space name..., pictures, etc)... Buyers can suggest an alternative space, when both agree on any space, the user that first recommended the space will be the one to pay the space fee before it can be scheduled/booked." No lat/lng field existed on any model, and no real distance calculation existed anywhere (core-matching's geographyMatch is country-code equality only) — this was greenfield, not a wire-up, and required deciding a distance-calculation approach with zero PostGIS/earthdistance precedent in this repo.New Branch.latitude/longitude/photos/amenities; new packages/core-geo (plain Haversine, no Postgres extension — see that package's README for why) backing business-service's GET v1/branches/nearby-spaces (200-mile default radius, a cheap bounding-box pre-filter narrowing candidates before the exact distance check), which also joins SpaceServicePricing (fees) and Review aggregates (ratings) directly — the founder's full listing-time requirement in one response. New Listing.preferredSpaceBranchId (set at listing time) auto-seeds the first TradeSpaceProposal row once a peer-to-peer TradeSession starts scheduling (Phase 26); the other party can counter-propose via trade-session-service's new POST /v1/trade-sessions/:id/space-proposals. Payer derivation directly implements the founder's rule: accepting a proposal sets SpaceBooking.bookedByUserId from whichever proposal's proposedBySellerId is the one that ends up ACCEPTED — i.e. whoever first proposed the winning Space pays, even if the other party is the one who clicks Accept. SpaceBooking creation itself is a genuine HTTP call from trade-session-service to space-service (the same class of exception as the pre-existing trade-session-service → reservation-service call — real side effects, Stripe payment-intent creation, that a direct Postgres write can't replicate), authenticated with a short-lived signed X-TBBN-Internal-Principal header, the same mechanism api-gateway already uses. This is what finally sets SpaceBooking.tradeSessionId, a nullable forward-compat hook since Phase 23 (item 19).Resolved
26Founder instruction: "Make sure that TBBN space will become calendly.com but for space. Make it a business on its own that will provide API services that will allow business/organization to reserve our space or provide our space reservations/booking services to themselves or their clients. Even though space reservation through marketplace limits it to 200 miles radius, space reservation through space.tbbnetwork.com and the API is open to all verified space from around the world and can be adjusted though filtering (country, state, ZIP/Postal codes, system location, general search, etc). These filtration can also be done through API configuration and set to return certain primary results." A follow-up AskUserQuestion round confirmed full headless booking (a business's own end client, with no TBBN account at all, gets a reservation made on their behalf) is in scope for this pass, not deferred. ApiKey (Merchant-only) and OAuthClient (also Merchant-only) had no structural path for a non-Merchant credential; no "verified Space" field existed anywhere; SpaceBooking.bookedByUserId was a required FK to User, with no path for a booking on behalf of someone with no account.New SpaceApiKey/SpaceApiKeyConfig models, rooted at Business (Phase 22's generic, Merchant-independent owner entity), owned by space-service — a new sk_space_live_/sk_space_sandbox_ credential prefix (packages/core-auth/src/space-api-key.ts, reusing ApiKey's hash/verify primitives, same "shape reuse, not a shared identifier space" convention as oauth-client.ts), validated by a new branch in credential.middleware.ts that sets principal.spaceApiKeyId/businessId (additive fields on the existing flat InternalPrincipal interface). New GET /v1/space/search in space-service, deliberately separate from Phase 31's nearbySpaces (which stays exactly as-is, 200-mile-defaulted, for the marketplace trade flow) — unbounded unless radiusMiles is given, filterable by country/region/postalCode/lat+lng/free-text query; presenting a valid key applies that key's SpaceApiKeyConfig defaults and always sorts its pinnedBranchIds first (isPinned: true) — the founder's "API configuration... return certain primary results" clause, implemented literally. "Verified Space" scope decision: Branch.spaceStatus === 'SPACE_ENABLED' is this pass's de-facto verification gate — no new field, no real moderation workflow, since ops-console has no staff-authentication principal type to operate one against; flagged in search.controller.ts and the new Space API guide, not silently treated as real verification. Headless booking: SpaceBooking.bookedByUserId made nullable (mirrors Phase 27's Seller.userId reversal exactly), with new bookedByBusinessId/externalClientRef (the consuming Business's own opaque client identifier, mirroring merchantSellerRef's shape) and a new paymentLinkToken — an unguessable token backing public hosted payment (space.tbbnetwork.com/pay/:id) and confirm/cancel access with no TBBN session at all, the same "a token grants access without a session" shape auth-service's existing magic-link pattern already uses. handlePaymentWebhook needed no changes — it already never inspected who booked. TBBN Space payment still only ever happens on TBBN's own site (TBBN_Flow.docx §7.3) — a headless booking's paymentUrl is the only way to pay it; this does not create a third TBBN money flow, only a way to reach the existing Space payment flow without a TBBN login. Explicitly flagged, not decided: Space API monetization (an open item in docs/business/pricing.md only — any new fee needs founder sign-off per items 1/16) and the new liability question a headless-booked client (with even less of a TBBN relationship than a headless Seller) introduces (docs/legal-flags/open-legal-questions.md).Resolved
27Founder instruction: "Change doc.tbbnetwork.com to developer.tbbnetwork.com so it will /merchant/doc and /space/doc." Repo-wide grep confirmed only the plural docs.tbbnetwork.com is used anywhere (the founder's singular is shorthand for the same thing) and it is hardcoded in ~15 files across every app plus several narrative docs, several as deep links (e.g. /guides/pricing, /api/apikeys) whose actual path also needed to change, not just the domain. apps/docs-site had a single flat nav with no product/section concept to split on — confirmed no existing app in this repo splits into two path-sectioned products, so this was new structure, not a pattern to copy.apps/docs-site restructured into real URL segments /merchant/docs/* and /space/docs/* (not route groups, per the founder's literal /merchant/doc//space/doc ask) — lib/docs-manifest.ts's GuideEntry gained a product field (all 20 pre-existing guides 'merchant'; one new docs/api/space-api-guide.md authored fresh, 'space'), lib/spec.ts gained SPACE_TAGS/isSpaceTag to split the OpenAPI-tag-driven API reference the same way. Shared logic factored into product-parameterized components (GuidesIndex/GuidePage/ApiIndex/ApiResourcePage) rather than duplicated between the two trees. SDKs/Changelog aren't product-specific, so /space/docs links out to the /merchant/docs copies rather than duplicating those routes. Root / becomes a two-card chooser. Every hardcoded docs.tbbnetwork.com occurrence was updated with both the new domain and, where it carried one, the new path (e.g. docs.tbbnetwork.com/guides/pricingdeveloper.tbbnetwork.com/merchant/docs/guides/pricing) — a domain-only find/replace would have left every deep link broken. While touching the shared packages/ui SiteFooter and docs-site's own header nav, also fixed the pre-existing stale exchange.tbbnetwork.com references (Phase 29 already consolidated exchange-web to root tbbnetwork.com) and added the account/business/space.tbbnetwork.com properties that were missing from that list — an adjacent accuracy fix surfaced by, not required by, this rename.Resolved
28Founder instruction (Phase 34): "All payments for space are to be done on our website and we can send a response/webhook with all the relevant and requested information. If a business decides to make their space free, they will be paying $20 for each space that they set up, if they charge for the space, they will not have to pay anything other than the set commission... Businesses can decide if their space is to be listed to the public, or only accessible through API or TBBN users with certain verified emails... Third party businesses decide when all required consent has been given, who pays, and when... After their business/school/organization has been verified, they or other designated roles in account.tbbnetwork.com will then proceed to space.tbbnetwork.com to set up spaces for study rooms/meeting rooms/office rooms/etc." The founder explicitly asked for a dynamic (not hardcoded) fee set by TBBN staff via a new admin.tbbnetwork.com, but also explicitly said "Do not build it now, just plan it out."Introduced Space as the actual bookable unit within a Branch (a Branch stays the location/host-Stripe-account/commission-rate level) — SpaceServicePricing/SpaceBooking moved from branchId-rooted to spaceId-rooted; each Space carries its own hours (weeklyHours), concurrent-booking capacity (maxConcurrentBookings, read as overlapping capacity, not a period cap), photos, notes, structured parking fields, an explicit isFree flag, and a visibility enum (PUBLIC/API_ONLY/RESTRICTED_EMAIL, the last backed by both an exact-email allowlist and a domain match — confirmed with the founder). Added Business.verificationStatus (PENDING/VERIFIED/REJECTED), enforced at Space-creation time and defense-in-depth at Branch.spaceStatus flip time; the actual review action is an interim unauthenticated ops-console page (/businesses), same "ops-only in practice, no TBBN_OPS principal yet" convention as every prior ops action in this repo, pending admin.tbbnetwork.com. Added BusinessUser/BusinessUserRole (mirroring MerchantUser/MerchantUserRole exactly) so "designated roles," not just the single owner, can manage Spaces/API keys/team membership. Added SpaceApiKeyConfig.consentMode (STRAIGHT/REQUIRE_CONSENT, overridable per booking request) plus a new AWAITING_CONSENT booking status and a token-gated hosted consent page (space.tbbnetwork.com/consent/:id) for the headless path — mirrors the existing paymentLinkToken "a token grants access without a session" shape. Confirmed while implementing that the existing PENDING_PAYMENT → PAID webhook flow already tolerates grossAmount: 0 unchanged, satisfying "a free space must still be confirmed on our page" — but found and fixed a real pre-existing gap where a logged-in, non-headless booking was never routed to any pay/confirm page at all (browse/page.tsx redirected to / instead). Added a new Business-rooted outbound webhook mechanism (BusinessWebhookSubscription/BusinessWebhookDelivery) satisfying "we can send a response/webhook with all the relevant and requested information" — kept as its own model/module pair (not a retrofit of the Merchant-rooted WebhookSubscription, per this repo's "each credential/entity type is its own module" convention) while extracting the existing HMAC signing/retry loop into a shared delivery-kernel.ts so the mechanics aren't reimplemented. The $20 free-listing fee is real and DB-driven (PlatformFeeSchedule, BusinessOneTimeCharge created PENDING at Space-creation time) but deliberately not collected or admin-editable this passbilling-service has no one-time-charge mechanism at all, and building one now would be exactly the "silently build speculative billing enforcement" pattern this repo already avoids (see pricing.md's grace-period note); both collection and a real admin UI wait on admin.tbbnetwork.com (Phase 35), which per the founder's own instruction is planned in the roadmap but not built this pass.Resolved
29Founder instruction (Phase 36): "Make sure that our current set will allow restaurant owners or other small business owner who are offering tables or other small corner spaces can still fit into our model TBBN space model... TBBN space business model should have the features of both Airbnb and Calendly." Separately: "Marketing-site and exchange-web need to collapse into tbbnetwork.com" with a specific eBay-style landing page (dual hero sections for Trade-by-Barter and TBBN Space, live listing cards with filters, a public space.tbbnetwork.com/listings route). Exploration confirmed Phase 29's domain consolidation had already moved exchange-web to root tbbnetwork.com and folded marketing-site's home-page sections in, but 4 marketing-site pages (how-it-works/solutions/pricing/company) were flagged in marketing-site's own changelog as needing a rewrite and were never actually migrated — genuinely still orphaned, not a false alarm.Added Space.category (STUDY_ROOM/MEETING_ROOM/OFFICE_ROOM/EVENT_SPACE/RESTAURANT_TABLE/RETAIL_CORNER/PARKING_SPOT/OTHER) as a purely browsable/filterable label — booking mechanics untouched — plus checkInMethod (SELF_SERVICE_NUMBERED/SIGNAGE/STAFF_ASSISTED/OTHER) describing how a guest finds their reserved spot on arrival, confirmed with the founder as the right layer: the existing maxConcurrentBookings already covers "multiple identical spots" (five tables, not one), and a genuinely distinct numbered room is just a separate Space listing, as today — no new per-spot sub-entity needed. Added the same category filter to both GET /v1/space/search (Phase 32) and GET /v1/branches/nearby-spaces (Phase 31) for parity. Found and fixed a real, separate bug while wiring exchange-web's new Space section: packages/sdk-typescript's TbbnClient (the facade @tbbn/sdk-next's getServerClient() returns for Next.js Server Components) had no space namespace at all and was missing branches.nearbySpaces — every Phase 23/31/32/34 Space method existed only on @tbbn/sdk-js's base client, silently blocking any server-side Space fetch from an app using the sdk-next facade. Fixed by adding the missing passthroughs (same pattern as the existing catalog/media/directory passthroughs). Renamed space-dashboard's /browse to /listings (matching the founder's own naming) — confirmed it was already fully public for search+view with only booking session-gated, so this was a rename plus URL-param deep-linking, not new auth logic; "public for everyone, personalized when logged in" was already the existing behavior. Built a new SpaceListingsTeaser on exchange-web's home page (real Space search results via the now-fixed sdk-next path, falling back to hand-authored demo data on failure, same convention as the existing DEMO_LISTINGS) with a plain cross-domain GET form/links into space.tbbnetwork.com/listings — not in-page filtering, since Space listings live in a separate app/domain from the item marketplace's own BrowseGrid (which already fully satisfied the "items" half of the landing-page spec and was left unchanged). Migrated marketing-site's 4 orphaned pages into exchange-web as real routes, rewritten in that app's own local design system (not @tbbn/ui, which exchange-web deliberately diverges from), fixing along the way: every stale /#waitlist CTA (exchange-web has real live registration/business-onboarding now) and company/page.tsx's stale exchange.tbbnetwork.com self-reference (Phase 29 already moved this app to root tbbnetwork.com). marketing-site itself is untouched — still SUPERSEDED/undeployed reference code, just no longer describing these 4 pages as pending migration in its own changelog.Resolved
30Founder instruction (Phase 37): "Rename exchange-web to Landing-page. Redesign the heading and the hero section to look like the one I have in this image [a real 'Let's Trade!' composite photo, plus a hand-drawn wireframe: nav → a boxed hero with a corner 'Trade Now' CTA → one row of item cards]. Use the you have it image as the hero image and a 'Trade Now' button that will lead to tbbnetwork.com/marketplace. Below the hero image will be a section of the top trading items with a few cards that will not bleed into a second row, just 1 row. Eliminate the current trade cards and filtering, all that should belong to tbbnetwork.com/marketplace." This directly un-does part of Phase 36's approach, which had folded the full BrowseGrid (search/category/sort) directly onto the home page under the hero.apps/exchange-web renamed to apps/Landing-page (git mv, package renamed @tbbn/landing-page, .claude/launch.json entry updated, port 3200 unchanged) — every other reference to exchange-web repo-wide was updated for files describing current-state architecture (docs, READMEs, cross-app comments); dated entries in this log and in docs/phase-plan/roadmap.md were deliberately left as-written, since they're a record of what was true at the time, same as this repo never rewrites past commits. New components/Hero.tsx replaces the old ProductArt mosaic hero with the founder-provided photo (public/hero/lets-trade.png) in a rounded full-width container, a short on-page heading/subcopy above it (distinct text from the image's own baked-in "Let's Trade!" copy, since the image itself isn't accessible/crawlable), and a single "Trade Now" CTA overlaid bottom-right on the image over a gradient scrim, linking to /marketplace. New components/TopTradingItems.tsx renders a single horizontal-scroll row (flex overflow-x-auto on fixed-width cards, not a wrapping grid — a grid can still wrap to a second row on a narrow viewport, which a scroll strip cannot) of real listings via a newly extracted lib/listings.ts (the loadListings() fetch, previously inlined in app/page.tsx, now shared by both the home page and the new marketplace page). The full BrowseGrid moved wholesale to a new app/marketplace/page.tsx — the home page (app/page.tsx) no longer does any search/filter/sort of its own; nav and footer "Browse" links were repointed from / to /marketplace to match. AudienceSplit/HowItWorks/SpaceListingsTeaser/SpaceExplainer/SafetyTips (Phase 36's Space-side landing content) were untouched — this pass only touched the Trade-by-Barter hero and the grid beneath it.Resolved
31Founder instruction (Phase 38): "Delete and clean out marketing-site after you have confirmed that all the important information from there can now be found in the landing-page except for the coming soon parts." Separately, on Landing-page: "eliminate the text between the header and the hero... turn the hero into a sliding hero and use the attached image (space hero)... as the second slide [with a] 'Learn more' button... that will lead to space.tbbnetwork.com... fix the space listing cards section on the landing-page to be similar to top trading items with no filtering and search option... recommended spaces based on location if provided, user behavior if logged in, or just random ones." Separately, on space.tbbnetwork.com: "it becomes a landing marketing page similar to tbbnetwork.com... will not mention barter trading... 'Get started' button... will either take the visitor to tbbnetwork.com sign in page, business profile set up, TBBN Space dashboard (if they already have space), or TBBN space onboarding page if it['s] their first one" — plus a second hero ("do you need a safe space") with a "Find a safe space" button to /listings, "both public and gated." Explicitly asked to "fill in the gaps in my logics."apps/marketing-site deleted entirely — a research pass confirmed every substantive page was already migrated (home sections in item 23, how-it-works/solutions/pricing/company in item 29); everything left was coming-soon/waitlist scaffolding. packages/ui's SiteNav/SiteFooter (previously only consumed by marketing-site) were not removed as dead code — they became the new marketing chrome for space-dashboard's rebuilt home page instead. Landing-page's components/Hero.tsx became a 2-slide client-side carousel (plain useState/setInterval, no new dependency) — the "Trade by barter" tag/heading/subcopy block was removed entirely (nav is now directly followed by the hero; a sr-only <h1> covers accessibility since the image's own text isn't crawlable); slide 2 is the founder's safe-space image, CTA "Learn more" → https://space.tbbnetwork.com. SpaceListingsTeaser lost its search form and category quick-links entirely, rebuilt as a TopTradingItems-shaped single horizontal row. The "based on location/behavior/random" recommendation ask was implemented honestly, not fabricated: no location-on-profile or view/behavior-history model exists anywhere in this schema, so "user behavior when logged in" has no real signal to key off — flagged as a genuine gap for a future recommendation-service phase rather than faked. "Location if provided" uses a real signal instead: a hosting platform's injected geo header (x-vercel-ip-country), passed straight through to space.search()'s existing country filter when present, omitted (i.e. "random"/default) otherwise — same cascade used on both Landing-page's and space-dashboard's own teaser. space-dashboard had a real, pre-existing architecture gap: it ran its own duplicate /login (localStorage session), directly violating this repo's own established Phase 29 rule that sign-in only ever happens on tbbnetwork.com — account-dashboard already followed that rule correctly via a cross-subdomain httpOnly tbbn_refresh cookie (Domain=.tbbnetwork.com) that Landing-page's login sets. Fixed by adding the same middleware.ts/lib/session.ts pattern (copied from account-dashboard) plus a new POST /app/api/session Route Handler that lets the browser silently adopt that cookie into this app's own localStorage (lib/tbbn.ts's new adoptCrossDomainSession()) — every existing dashboard page's own data-fetching was left completely unchanged, a deliberately low-risk, additive fix rather than a rewrite of already-working Space-management functionality. The local /login page was deleted; its redirects now point to tbbnetwork.com. The session-gated dashboard home moved from / to /dashboard to free up / for the new marketing page. New app/get-started/page.tsx resolves the founder's exact 4-state routing ask server-side in one request (no aggregate "has a Business/Branch/Space" endpoint exists anywhere, so it composes businesses.list() → per-business branches.list() → per-Space-enabled-branch space.spaces.list(), same calls the rest of the app already makes one at a time): no session → tbbnetwork.com login; no Business → business.tbbnetwork.com; a Business but no Space-enabled Branch → that business's branches page (where the spaceStatus toggle already lives); a Space-enabled Branch with zero Spaces → that branch's existing "Add a Space" form (already the onboarding page, nothing new built there); ≥1 real Space → /dashboard. New app/page.tsx (Space-only messaging, zero barter/trade wording) pairs SiteNav/SiteFooter with a new local HeroSlider (same interaction pattern as Landing-page's, styled against @tbbn/ui's shared palette instead — Landing-page deliberately has zero @tbbn/ui dependency, so this couldn't be one shared component) and a new SpaceTeaser (the same single-row, no-filter pattern). /listings was additionally wrapped in the same SiteNav/SiteFooter chrome for visual consistency with the rest of the now-marketing-styled app — its own search/booking logic was untouched.Resolved
32Founder instruction (Phase 39): "I will be using full AWS (EC2, S3, SES, etc), google map service, and stripe payment services for this project. Create a .env file with the name of ALL the information that I will need to provide to turn this into a fully functional application. Also complete the admin.tbbnetwork.com and all other remaining page. Remember, all the landing page for each domain is supposed to be a marketing page for the services offered by that domain. Also remember that sign in and sign up can only happen in main landing page (tbbnetwork.com). Build behavior tracking and recommendations engine for the marketplace and the space listings services. Important services in the marketplace can [use] the system location which means the consent mush be sought. Differentiate primary (basic) cookies and secondary cookies and ask for consent with option for preference."Root .env.example created cataloguing every AWS (S3 for media-service's StoragePort, SES for auth-service/notification-service's NotificationPort, SNS for SMS OTP), Google Maps (a server key for geocoding, a NEXT_PUBLIC_ key for embedded maps — neither exists in this codebase today, confirmed by grep), and Stripe (billing-service's StripePort, space-service's StripeConnectPort) credential this system will need once those Console/Local adapters are swapped for real ones — the adapter code itself is a separate, later effort, not done here. admin.tbbnetwork.com completed: new StaffRole enum + a staffRole field on the existing shared User model (reusing the already-registered-but-unused UserAuthType.TBBN_OPS value — no new StaffUser table needed, since a single role is enough for staff, unlike Merchant/BusinessUser's multi-org join tables), a new OtpPurpose.STAFF_LOGIN (reusing the existing generic OtpChallenge table), auth-service's requestStaffOtp/verifyStaffOtp mirroring the merchant-rep OTP pair exactly, a new OpsRolesGuard/@OpsRoles(...) in packages/core-auth (a sibling to the existing merchant-rep RolesGuard, kept separate to avoid any regression risk to that already-working guard), staff provisioning via a new tools/create-staff-user CLI (never an HTTP endpoint — no self-registration for staff, ever), and real guards added to the 7 previously-bare ops-only endpoints found (merchant approval ×2, Business verification ×2, moderation, fraud, feature-flags — plus a 7th, space-service's commission-rate override, found via its own doc comment cross-referencing the same "no guard yet" convention, which the original research pass missed). apps/ops-console itself: real 2-step OTP login (previously an inert disabled preview) and a real session gate on (console)/layout.tsx (previously none at all) — every existing feature page needed zero changes since they already called a single getOpsTbbnClient() choke point. merchant-dashboard and business-dashboard marketing pages: verified first, by reading each login page directly, which of the two actually violated "sign up/sign in only ever happens on tbbnetwork.com" — merchant-dashboard's login calls requestMerchantOtp/verifyMerchantOtp (a legitimate, distinct B2B principal kind, correctly local, not touched); business-dashboard's login called registerMember/requestSellerOtp/verifyMemberOtp/verifySellerOtp — the exact same marketplace-member endpoints Landing-page's own /register//login use, a real violation. Fixed business-dashboard with the identical cross-subdomain cookie pattern already built for space-dashboard this session (middleware.ts/lib/session.ts/app/api/session/route.ts/adoptCrossDomainSession()), copied file-for-file; its local /login deleted. Both apps' session-gated homes moved //dashboard; both gained a real marketing / page in each app's own voice (merchant/developer platform pitch vs. Business/Branch/Space program pitch) — no /get-started resolver needed for business-dashboard specifically, since it only has two states (signed in or not), unlike space-dashboard's four. Behavior tracking + recommendations: confirmed first that this was genuinely greenfield — no Event/View/Interaction model existed anywhere in the schema (only UsageEvent, which is merchant-rooted billing metering, not user behavior), and the pre-existing recommendation-service (already WORKING) was a pure item-attribute matcher with zero behavioral signal and zero Space awareness. Added one new BehaviorEvent model (eventType VIEW/SEARCH/CLICK, targetType LISTING/SPACE, nullable userId/visitorId) and extended (not replaced) recommendation-service with a no-auth POST /v1/recommendations/events ingestion endpoint and two "for-visitor" read endpoints (listings/for-visitor, spaces/for-visitor) that build a lightweight preference profile from a visitor's recently-viewed category/brand (listings) or category/region (Spaces) and re-rank the same candidate pool recommendations.service.ts already builds — falling back to a plain newest/recency feed on cold start (no view history), never a fabricated "personalized" result. The visitor id is a first-party cookie (not localStorage), deliberately, so TopTradingItems/SpaceListingsTeaser/SpaceTeaser (Server Components) can read it via next/headers on the next request — the very first page view for a new visitor still gets the cold-start feed, an honest limitation, not a bug. Wiring this up surfaced and fixed two real pre-existing bugs unrelated to this feature: apps/Landing-page/app/listings/[id]/page.tsx called getServerClient() outside its own try/catch (crashing the whole page instead of 404ing when the backend is unreachable), and the new SpaceTeaser had briefly reintroduced the same mistake before being caught in browser verification. Cookie & location consent: a client-side tbbn_consent value (essential always-true/no-toggle, analytics off-by-default) plus a bottom banner (Accept all / Reject non-essential / Manage preferences) and a preferences modal, built once for Landing-page and mirrored for space-dashboard (the two consumer-facing marketing sites — business-dashboard/merchant-dashboard deliberately left for a later pass to keep this bounded); trackView/trackSearch both check consent before ever firing, so a visitor who declines generates zero tracking network traffic, not just a server-side no-op. Location consent is a separate, narrower prompt (useLocationConsent) shown before ever calling the browser's own navigator.geolocation API, wired to a real "Find Spaces near me" control that feeds actual coordinates into space.search()'s already-existing lat/lng params — not a placeholder.Resolved
33Founder instruction (Phase 39 continued): first stood up apps/Landing-page on a real EC2 instance behind tbbnetwork.com (Route 53 A record), then, once AWS S3/SES/Google Maps credentials existed (Stripe and SMS/SNS deliberately excluded — "except sms and stripe credentials"): "Now wire up everything and get them ready for production... Finish all that we have left so we can start testing in production." Also created Route 53 A records for developer/admin/space/business/account.tbbnetwork.com pointing at the same elastic IP.Real adapters swapped in behind the existing ports — no caller code changed beyond the DI wiring itself, per each port's own "callers depend only on the interface" design intent from item 32: media-service's S3StorageAdapter (downloads and re-hosts a source image in S3; no resize/re-encode yet — width/height stay null, same as LocalStorageAdapter, until an image-processing library is added); auth-service's and notification-service's SesNotificationAdapter/SesGeneralNotificationAdapter (email only — sendSmsOtp/sendSms/sendPush still log, since no SNS/SMS/push provider is configured); business-service's new GeoPort/GoogleGeoAdapter (Branch creation now geocodes the submitted address to lat/lng via the Google Maps Geocoding REST API when the client didn't already supply coordinates — a genuinely new capability, not just a swap, since no geocoding existed before at all). Each of the four services now binds its port's injection token (STORAGE_PORT/NOTIFICATION_PORT/GENERAL_NOTIFICATION_PORT/GEO_PORT, all new Symbol()s) to the real adapter when its env var is set and the matching Console/Local stand-in otherwise, decided once at module-load time in each app.module.ts — this also fixed a latent DI smell from item 32 (and earlier): AuthService/NotificationsService previously injected ConsoleNotificationPort/ConsoleGeneralNotificationPort by concrete class, not by interface, so swapping the real implementation in without this change would have required touching every call site anyway, contrary to each port file's own doc comment. auth-service's dev-only DebugController (/v1/auth/debug/last-otp, already disabled outside NODE_ENV=production) deliberately still injects ConsoleNotificationPort directly — it only ever needs the console adapter's own getLastCode(), which isn't part of the NotificationPort interface. Production deployment infra added, since none existed: root ecosystem.config.js (pm2 process list for all 32 WORKING services plus the 6 apps with a live DNS record — Landing-page/docs-site/ops-console/space-dashboard/business-dashboard/account-dashboard; merchant-dashboard/status-page omitted, no DNS record yet), infra/docker/docker-compose.prod.yml (Postgres+Redis only — no OpenSearch/MinIO container, since no service in this repo actually reads OPENSEARCH_URL/MinIO credentials; confirmed by grep before dropping them, not assumed), and infra/nginx/tbbnetwork.conf.example (one nginx server block, hostname-mapped to the matching pm2 port, covering all six app subdomains plus a new, not-yet-created api.tbbnetwork.com record this deployment needs — every app's NEXT_PUBLIC_TBBN_API_BASE_URL must resolve over the public internet from the browser, not localhost). All 32+ processes deliberately share ONE gitignored infra/env/.env file rather than one per service — a documented, deliberate exception (see infra/env/README.md's new "Production" section) to the "no repo-wide shared .env" rule from item 32's own .env.example, justified purely by the operational cost of keeping 32 files' shared secrets (INTERNAL_SERVICE_SECRET/JWT_SECRET) in sync by hand; every service's zod schema silently ignores keys it doesn't declare, so this is safe, not just convenient. .env.example's stale "nothing below is wired yet" framing was corrected in place, per this repo's own convention of fixing current-state docs rather than leaving them stale.Resolved