A customer portal where buyers request samples and track shipping
Buyers sign in to request samples and check shipping, while staff review every request in one list. The steps from sample request to shipping work on test data. The portal is still being built; order and quote forms come next.
CreditDesigned and built by our founder, as lead developer for a partner agency.
Follow one sample request
The buyer follows the request while staff decide each step
Both sides see the same request, under one reference number.
Staff asked for a delivery phone number and the buyer replied. Staff then approved and shipped request SR-2026-0001. The buyer can see the carrier and tracking number TRACK-0001. Private staff notes are not shared.
About Copperstill Ingredients
When a buyer asked about an order, someone had to look it up in a spreadsheet and reply.
Copperstill Ingredients sells bulk spirits to licensed businesses in the US, in anything from a 4 oz sample bottle to a tanker. Most buyers order again and again. Staff kept each request's stage, tracking number, reminders and notes in one master spreadsheet, and customers had nowhere to sign in.
- Client
- Copperstill IngredientsA US bulk spirits supplier selling to licensed businesses
- Industry
- Bulk spirits · United States
- Buyers
- Licensed businesses, often repeat customers
- Sales process
- Samples, quotes and repeat orders

The problem
Buyers had to ask staff for every update
Buyers emailed or phoned to check on orders
To find out whether an order had shipped, a buyer had to email or phone someone.
Staff typed each request into a spreadsheet
Every request had to be copied into the spreadsheet by hand, then followed up.
Buyers could not see their history
There was no customer account showing what the company had asked for or received.
Permit paperwork was tracked by hand
Some orders must wait for the buyer's permit paperwork, and staff tracked these holds and reminders by hand. The product list also had to be updated in two places.
What was built
An account for buyers and a review list for staff
Buyers and staff follow the same request
A saved draft becomes a request with its own reference number. Buyers can answer questions and see shipping details, while staff move the same request through review.
A company account for the whole team
Buyers sign in with email or Google. Company admins invite or remove colleagues, and access depends on being a member of that company.
The full product list is ready to choose from
The catalogue holds 41 products in 7 categories and 6 container types. The sample form records products, sizes, quantities and a delivery contact.
Staff work from one list
Filters help staff find requests. They can ask buyers questions, keep private notes, record payments and add the carrier and tracking number.
Control and safety
Staff decide when a request moves forward
People review, approve and mark requests as shipped. Only the emails and one reminder are automatic. If a buyer does not answer a question, the request goes onto a staff follow-up list; it is not closed automatically.
- Company access is checked in several places. Both the app and the database check which company a user belongs to and their role. Tests try to reach a second company's records through pages, actions and downloads.
- Private staff notes are not shown to customers. A test checks that private notes do not reach customers. Actions staff take for a buyer are logged under the real staff member's name.
- Files are checked before use. Uploads are limited to 25 MB and six file types. The server checks what each file really contains, and every download goes through a route that checks access.
- Removing a colleague ends their access. Removing someone inside the portal takes effect on their next request. Removing them at the sign-in provider took about 68 seconds in three timed tests.
For technical readers
How the system works
One app serves both buyers and staff. Each action checks who is signed in and their role, then works only with that company's data. A change is saved before the email about it is sent.
Dashed lines show supporting connections, or parts that are paused or not yet built.
In more detail
Technology used (10 parts)Each part of the system, and why it was chosen.
| Layer | Technology | Why it was chosen |
|---|---|---|
| Interface | Next.js 16, React 19, Tailwind 4, shadcn/Radix | One app serves the customer and staff screens, with accessible controls and touch targets of at least 44px. |
| Actions | Next.js server actions and route handlers, Zod 4 | Each input is checked, and unknown fields are rejected, before any work runs. |
| Database | Supabase Postgres, Drizzle ORM, 37 migrations | Database rules check the company and environment, as well as the app's own filters. |
| Sign-in | Clerk | Handles email and Google sign-in and invitations. Membership and role are also stored in the portal's own database. |
| Background work | Vercel Cron and queue-page sweeps | Daily jobs and short checks retry waiting emails and send reminders. |
| Resend | Separate log, test-list and send modes keep test emails apart from real ones. | |
| Files | Private Supabase Storage, file-type | Uploads go straight to private storage and are checked by their content; downloads go through a route that checks the user is signed in. |
| Hosting | Vercel | Preview, staging and live settings are kept separate, and every database row records its environment. |
| Checks | GitHub Actions, Biome, oxlint, TypeScript 7 | Code style, types, the build and the database schema are checked on every change. |
| Tests | Vitest, Playwright, Postgres 17 in CI | Tests use a real database and browser sessions for two made-up companies to check the access rules. |
5 engineering decisions that matteredThe problem, what was done, and what it changed.
A forgotten filter must not show another company's data
One helper sets the company, user, role and environment for each database transaction. Database rules require that information, and the app uses a database account with limited rights. The app's own filters stay as an extra check.
Access is removed in the portal first
When someone is removed, the portal first marks their membership as revoked. Every request reads that record, so their next request is refused. The call to the sign-in provider is retried afterwards. Signed timestamps on events stop an older member-added event from restoring removed access.
Every download is checked
Files are sent through a route that checks access, rather than through a reusable download link. Uploads go straight to private storage using a one-time link, and then the server checks the real file type. A PNG image renamed to look like a PDF is not accepted.
A change and its email are saved together
A status change saves a waiting email in the same database transaction, and the email is sent afterwards. Background workers pick up emails without overlapping, and a key for each recipient prevents duplicate emails when a send is retried. Updates about a request that has since closed are skipped.
Samples, orders and quotes share one structure
Samples, orders and quotes share one requests table. Fields specific to each form use a versioned layout, so old requests stay readable. Both the app and the database control which status changes are allowed. Only the sample form is built in this record.
Testing and build recordHow it was tested, and the build by the numbers.
Quality and reliability
- The last recorded full local run passed 771 unit tests. A further 195 database tests run against Postgres in the automated build checks (CI); the source describes 960+ automated tests in total, plus browser checks.
- Browser tests (Playwright) in 18 files sign in as real test users of two made-up companies, including checks against direct links, actions and downloads.
- Three timed tests of removal at the sign-in provider first refused access after 67.6, 67.8 and 69.1 seconds, checking every 10 seconds. Removal inside the portal is different: it takes effect on the next request.
- The automated checks also confirm that the 37 migrations still match the database schema, alongside code style, types, build, unit, database and browser tests.
- Failed emails retry six times, with waits of 1, 2, 4, 8 and 16 minutes. Stored error codes leave out email addresses and secrets.
By the numbers (build)
- 258 commits and about 50 pull requests from 20 September to 3 October, around 65,000 lines of TypeScript.
- 37 database migrations and 80+ database access rules (row-level security).
- 4 roles and 3 request types in one model; only the sample form is built.
- 16 days from the walkthrough to a support conversation built and tested on the staging version, not yet released.
Is it a fit?
Could this work for your team?
- You supply businesses that order again and again. Buyers ask for samples, quotes and orders by email or phone.
- Your team tracks requests in a spreadsheet. Staff copy details between tools and answer the same shipping questions.
- A person needs to check requests. Paperwork, payment or delivery details decide when a request can move forward.
- Your products and rules are different. We can adapt the catalogue, units, request fields and email wording, and connect the portal to your stock and pricing systems.
Do buyers still email to ask where their order is?
Conducted by Apurva · Not a sales call
Or write to care@brownmind.com