Security
Each shop sees only its own records.
Access is decided in the database, not on the page. Whoever signs in sees exactly what their job is allowed to see, and anyone whose access cannot be resolved sees nothing rather than everything.
- Shop isolation
- PostgreSQL row-level security
- Database
- Contabo · EU (France)
- SOC 2
- Not certified
- Security contact
- laurence@bien.mxWhatsApp +52 614 479 2338
Request scoping
The database decides, row by row.
Every shop’s records share one database, and each row carries the shop it belongs to. The browser never names the shop: the database looks it up from whoever is signed in and applies the policy to every row it could return.
01 · Browser
Signed in, given Location A
GET /rest/v1/work_ordersAuthorization: Bearer ‹your session›The shop is not part of the request.
02 · PostgREST
Checks the session, runs as you
role = authenticatedauth.uid() = ‹you›03 · Row-level security
Checks every row
current_tenant_id() IS NOT NULLYour shop, from your membership Passes.AND tenant_id = current_tenant_id()Another shop’s rows drop out Passes.AND deleted_at IS NULLDeleted rows stay out Passes.AND (has_global_role(ARRAY['owner', …])The shop’s owner Not this person.OR location_id = ANY (user_location_ids())A location the shop gave you Admits this person.OR EXISTS (… technician_id = auth.uid() …))An active job assigned to you Not this person.
04 · Rows returned
2 of 4
- Your shopLocation A · active, assigned to youReturned
- Your shopLocation AReturned
- Your shopLocation BWithheld · Location not given
- Another shopAny locationWithheld · Another shop
work_orders, abbreviated from BayStacker’s schema. The rows are a diagram, not records.Evidence trail
Who changed what, and when.
A change to a customer, a vehicle, a person’s profile or a legal entity writes an audit row. Owners and admins read it in the shop’s audit log.
tenant_id- The shop
table_name · record_id- The record
action- Insert, update, archive or delete
old_values → new_values- Before and after
changed_by- The signed-in person
changed_at- When
And some records cannot change at all.
An analysis, once written
UPDATE analysis_results SET hypotheses_json = …ERROR analysis_results content is immutableA purchase order’s lines, once it leaves draft
UPDATE purchase_order_lines SET unit_cost = …ERROR FYV635_SUBMITTED_PO_LINES_ARE_IMMUTABLEThe shop operations log
DELETE FROM shop_operations_audit_log WHERE …ERROR shop_operations_audit_log is immutable — DELETE is prohibited.
Gates
A gate is a refusal, not a hidden button.
On a repair with a diagnosis case BayStacker manages, each step is refused until its evidence is on the record.
01
Commit intake
A captured, decoded 17-character VIN.
Refused by the database
VIN_REQUIRED 02
Invoice
Post-repair verification passed, or a manager’s recorded waiver.
Refused by the server
VERIFICATION_REQUIRED 03
Vehicle release
A verification result — a waiver only with its scope and reason — on an invoiced work order.
Refused by the database
VEHICLE_RELEASE_ VERIFICATION_ REQUIRED 04
Case closeout
The latest verification passed after the conclusion: road test, code recheck, QC checklist and evidence, with a named verifier.
Refused by the database
PASSING_VERIFICATION_ REQUIRED
What it does and does not do
Stated both ways.
| Area | BayStacker does | BayStacker does not |
|---|---|---|
| Shop isolation | Decides what a browser may read in the database, with row-level security keyed to the signed-in person. | Take the shop from the request, or fall back to showing everything when access cannot be worked out. |
| Privileged key | Keeps the database’s privileged key on the server and uses it there — for example, to open a customer’s secure link. | Send it to a browser. |
| Money | Shows money by job: full, limited or none. A technician sees none of it. | Recalculate rebuild labour or parts tax — the shop’s own rules apply. |
| Payments | Records what a customer owes and paid on the invoice. | Process card payments. |
| AI | Holds Analysis A as a draft until a person confirms it. | Treat a model’s answer as the conclusion. |
| Unknowns | Shows a missing measurement, an unknown payment or an unreceived part as exactly that. | Invent a measurement, render an unknown payment as a success, treat a purchase order’s status as receipt, or a core shipment as a recovered credit. |
Security summary
Report a vulnerability or a suspected incident to laurence@bien.mx WhatsApp +52 614 479 2338. The same contact is published in security.txt.
- SOC 2
- Not certified. BayStacker has no SOC 2 report.
- Penetration testing
- None yet. No independent penetration test of BayStacker has been run.
- Data location
- The database runs on a server hosted by Contabo in the EU (France). The web application and its API are served by Netlify.
- Server access
- BayStacker’s server holds a privileged database key and uses it on the server side, for example to open a customer’s secure link. The key is never sent to a browser: a browser reads only under the signed-in person’s own access rules.
- Sub-processors
Service What it handles Contabo Hosts the database server (EU, France). Supabase Database software, self-hosted on that server. Netlify Serves the web application and its API. Stripe Subscription billing for the shop’s own BayStacker plan. Resend Sends account and customer email. Twilio Sends text messages. Sentry Receives application error reports (US). TypeSafe Adjudicates the candidate causes in a diagnosis, from the repair facts. OpenRouter (DeepSeek) Model calls TypeSafe does not take, from the same repair facts.
Bring your reviewer’s questions to a walkthrough.
Or send them to laurence@bien.mx.