The order started as a DM, so the customer record has to come from the conversation
A multi-tenant inbox for shops that sell through social messages, where a conversation becomes a linked customer, an order and a delivery without the operator opening a second system. The AI suggests replies; a person sends them.
What the engagement involved
- Industry
- E-commerce & Retail
- Duration
- 16 weeks
- Cooperation model
- Dedicated team
- Services
- SaaS engineeringAI integrationPlatform
- Integrations
- Social platform messaging APIsIdentity provider webhooksLanguage model APICourier tracking APIsSubscription billing
- Technologies
- Next.jsTypeScriptPostgreSQLManaged AuthTailwind CSS
- Team
- 1 Product lead2 Full-stack engineers1 Product designer
Client name withheld under NDA. Engagement details are shown to the extent our agreement permits.
What we were brought in to do
The vendor sells software to shops whose whole business runs in social direct messages. We built the platform: a shared inbox across connected accounts, customers linked from the threads themselves, orders and deliveries, and an AI that drafts replies for a person to send.
A software vendor selling to shops whose entire business runs in social direct messages. In those shops the thread is the system of record: the order is in a scroll, the address is four messages up, and only whoever was on the phone that day can answer whether it shipped. Every tool they had tried assumed an order arrives from a checkout.
Full-Stack Engineering
Where the old way broke
In a shop that sells by DM, the thread is the system of record. The order lives in a scroll, the address is four messages up, and the only person who can answer 'did we ship it' is whoever was on the phone that day. Every tool they had tried assumed the order arrived from a checkout.
The consequence is that the business's history lives on a platform the shop doesn't control and can't query. A staff member leaving takes their threads' context with them; an account revoked takes the record. And the operational questions (who is owed what, what's outstanding) can't be answered at all without scrolling, so they mostly go unanswered.
Make the conversation produce the record. Linking a handle to a customer is a single action from the thread, an order can be raised from the messages that describe it, and the delivery hangs off the order, so the thread stops being the only place the truth exists.
Auto-link from the thread
A handle becomes a customer record in one action, so the conversation stops being the only place the customer exists.
A handle becomes a customer record in one action from inside the thread, matching on prior orders and contact details where they exist and creating a record where they don't. That single action is the whole point: until the customer exists outside the conversation, the conversation is the only place the business's history lives, and it disappears with the platform.
- Handle to customer record in one action, from the thread
- Matches existing customers on orders and contact details
- The history stops living only inside the conversation
Linking a handle to a customer in one action: an existing customer matched on the name and prior order mentioned in the thread, and every field showing the message or record it was lifted from.
An order built from the two messages that agreed it: items, quantities and agreed price from one, the customer's yes and address from the other, tagged in the thread without another system being opened.
Orders raised in place
An order is created from the messages that agreed it, without switching to another screen.
An order is raised from the messages that agreed it, without leaving the thread: items, quantities and the agreed price captured while the conversation is still on screen. Switching to another system to record what was just agreed is the step that doesn't happen when a shop is busy, and everything downstream depends on it happening.
- Orders created in place, without leaving the thread
- Items, quantities and agreed price captured in context
- Removes the step that gets skipped when the shop is busy
AI drafts, people send
A reply drafted from the catalog and the last six messages, waiting in the composer for a person to send, with the one sentence that reads as a delivery promise marked to check first.
Suggestions appear in the composer. Nothing reaches a customer without someone choosing to send it.
Suggested replies appear in the composer as editable text and never send themselves. The line is drawn there deliberately: a wrong price or a promised delivery date sent automatically to a customer is a commitment the business has to honor, and no accuracy figure makes that an acceptable risk. Every message that reaches a customer was chosen by a person.
- Suggestions land editable in the composer
- Nothing sends without a person choosing to
- The line drawn at commitments, whatever the accuracy
The platform's tenancy view: a handler written with no tenant filter, the scope the data layer adds to its query, a cross-tenant probe that sees nothing, and every endpoint scoped below its handler.
Tenant scoping below the handlers
Isolation is enforced at the data layer, so a new endpoint can't forget it.
Tenant scoping is enforced in the data layer beneath the handlers, so a new route added next year can't forget it: the query simply can't return another tenant's threads. Putting the check in each endpoint would make the system's security depend on every future developer remembering, and that's not a property worth having.
- Isolation enforced below the handlers, in the data layer
- A new endpoint can't forget to scope
- Security doesn't depend on future memory
Revocation as an event
A disconnected account is known from a webhook the moment it happens, well before a message fails to send.
A revoked or disconnected account is known from the platform's webhook the moment it happens, so the inbox says so before a reply can fail in front of a waiting customer. Reconnection is prompted with the specific account named, and queued messages are held, never silently dropped.
- Revocation known from the webhook, ahead of any failed send
- The specific account named in the reconnect prompt
- Queued messages held, never dropped
A revoked account known from its webhook the moment it happened: the account named in the reconnect prompt, the event log, and the replies held in their threads, none dropped.
What we built together
Built customer linking as one action from the thread, because a separate 'create customer' form doesn't get used
Linking a handle to a customer is one action from inside the thread, because a separate create-customer form is a form nobody fills in when the shop is busy.
Kept the AI on suggestion only: it drafts, a person sends
The AI drafts into the composer and never sends, so every message reaching a customer was chosen by a person.
Made orders raisable from a conversation without leaving it
Raising an order in place was the feature the shops tested first and the one that decided whether the product was worth opening.
Scoped every query by tenant at the data layer, below the handlers
Scoping was moved below the handlers early, while there were four endpoints instead of forty, since retrofitting it later means auditing every one of them.
Handled account connection and disconnection through webhooks so a revoked token surfaces immediately
Account connection and revocation are known from webhooks, so a disconnected account surfaces before a reply fails in front of a waiting customer.
Operational results after launch
1
Systems to raise an order
1
Actions to link a customer
0
AI messages sent automatically
None needed
Tenant filters in handlers
Systems to raise an order and actions to link a customer are both one. AI messages sent automatically is zero by construction: the composer requires a person. Tenant filters needed in handlers is none, because scoping is enforced a layer below them.
Client name withheld under NDA. Figures are approximate, drawn from the engagement’s own reporting.
Phase by phase
Phase 1: Tenancy
Scoped At The Data Layer
Built tenant scoping below the request handlers, so a missing filter in a new endpoint can't leak another shop's conversations. Adding a feature doesn't mean re-remembering the rule.
- Tenant Model
- Scoped Data Access
- Admin Tenancy View
Phase 2: The Inbox
Accounts And Conversations
Connected social accounts through webhooks and pulled their threads into one inbox, with revocation handled as an event the moment it happens, long before a send can fail.
- Account Connection
- Unified Inbox
- Revocation Handling
Phase 3: Customers
Linked From The Thread
Built customer auto-linking: matching a handle to an existing customer where one exists and creating one in a single action where it doesn't, from inside the conversation.
- Auto-Link
- Customer Record
- Conversation History
Phase 4: Orders And Delivery
Raised Where They Are Agreed
Built order creation from the thread and delivery tracking against the order, so the answer to 'did we ship it' lives on a record, not in someone's memory of a chat.
- Order From Thread
- Delivery Tracking
- Order History
Phase 5: AI Suggestions
Drafts, Never Sends
Added reply suggestions grounded in the shop's own catalog and the thread so far. The suggestion appears in the composer; sending is always a person's action.
- Reply Suggestions
- Catalogue Grounding
- Human Send
A customer record made of conversations: seven orders summing to the lifetime value, no form, and the delivery on today's order with its address traced back to the message it came from.
About our collaboration
Three of their own merchants sat in the reviews every two weeks, which is how the AI ended up on suggestion only. They were clear that in a channel where tone is the relationship, a wrong automated reply costs more than a slow one. Every automation proposal after that had to survive the same question.
A dedicated team for sixteen weeks, working against shops that were live on the platform from week five, well before handover. That's what settled the AI boundary: watching an operator read a suggested reply before sending it made clear how often the draft needed a small correction that only they could make.
What we'd carry into the next one
- 01
When the thread is the system of record, the product's job is to get the record out of the thread and leave the thread alone.
The shops were never going to abandon DMs, so a product that competes with the thread loses. One that extracts the record from it is useful immediately.
- 02
A separate 'create customer' form is a form nobody fills in; linking has to be one action from where the operator already is.
A create-customer form asks an operator to leave the conversation during the busiest moment, which is exactly when they won't, so linking had to be one action.
- 03
AI that sends is a liability in a channel where tone is the relationship. AI that drafts is a head start.
In a channel where tone is the relationship, an automatically sent message is a commitment the shop has to honor. Drafting saves time; sending creates liability.
- 04
Enforce tenant isolation below the handlers, or every new endpoint is a fresh chance to leak.
Isolation enforced below the handlers means security doesn't depend on every future developer remembering, and that's not something worth betting a tenant on.
One thread, five steps, zero automatic sends
The record comes out of the conversation. The reply goes back only when a person sends it.
Step through a DM that becomes a linked customer, an order and a reply. Every field shows the message it was lifted from, and the draft waits in the composer until you press Send yourself. Switch tabs, or use the arrow keys once one is focused.
Reply to @rosa.delgado.home…
Unlinked handle
A handle, not a customer yet
Until this handle is linked, the conversation is the only place this customer exists. Stallbook has spotted a likely match in #m2, but nothing is linked until someone chooses.
- Handle@rosa.delgado.home · Photo appon record
Hover or focus a field to light the message it was lifted from.
An illustrative thread: the shop, customer, prices, address and times are invented to show the mechanism. The steps and the zero are the product’s behavior.
From a DM on a connected account to a record the shop owns
The thread stays where the shop sells. What the business needs to know is pulled out of it into records that belong to one tenant, and nothing goes back to a customer without a person.
- 01 · SourceConnected accounts, via webhooksConnection and revocation arrive as events, so a revoked token surfaces the moment it happens, before any reply can fail to send.
- 02 · InboxOne queue across accountsEvery connected account's threads land in one inbox. While an account is disconnected, queued messages are held, not silently dropped.
- 03 · RecordCustomer, order, deliveryA handle links to a customer in one action, matching on prior orders and contact details. Orders are raised from the messages that agreed them; delivery hangs off the order.
- 04 · DataPostgreSQL, scoped by tenantTenant scoping is enforced in the data layer beneath the handlers, so a new endpoint can't forget it: the query can't return another shop's threads.
- 05 · ReplyComposer + human sendSuggestions are grounded in the shop's catalog and the thread, and land as editable text in the composer. Sending is always a person's action.
Right tenant, right words, right door
Isolation below the handlers, drafts that wait & addresses with a source
One shop can't see another's customers
Tenant scoping is enforced once, in the data layer beneath the request handlers, instead of being remembered in each endpoint. A route added next year inherits it; the query simply can't return another tenant's threads.
No price or promise goes out unread
Suggested replies are drafted from the shop's own catalogue and the thread, and land as editable text in the composer. They never send themselves: every message that reaches a customer was chosen by a person.
The address comes from the conversation
Orders are raised from the messages that agreed them, and each captured field shows the message it was lifted from. The delivery hangs off that order, so what shipped where is on a record, not in someone's memory of a chat.
Selling through DMs, or putting AI next to a send button? Scope your build in 3 minutes.
Scope your buildNearby engagements
Data & AnalyticsA checkout that stopped losing sales to a 6-second load
Profiling found the real bottleneck behind a slow checkout (an N+1 query and an oversized bundle) and tuned both, with before-and-after metrics locked in as a baseline against future regressions.
E-commerce & Retail · 5 weeks
Web PlatformsA reader people finish, and a library that remembers where they stopped
A reading platform for a comics catalog: a browse surface people can actually navigate, a reader that gets out of the way, and a history that puts everyone back on the page they left.
E-commerce & Retail · 18 weeks
Web PlatformsOne number, a whole handset, and a database that keeps up with 125 brands
A metered IMEI lookup service that turns fifteen digits into a device, its specifications and its status. It's sold three ways to three audiences and backed by a device database that maintains itself.
Consumer Electronics · 22 weeks
Let's talk
Running a large platform, shaping a first MVP, or getting a product ready for a funding round? Tell us where you are. We'll shape the process around it, and stay with you after launch.














