Skip to main content
The notes we keep for ourselves about the pieces underneath. Nothing here is a secret — it is how the API decides what a token may reach — but it is written for whoever maintains it rather than for somebody building against it.

Who may act for which business

App\Modules\API\Auth\SellerScope answers it, and it is the only place that does. That last difference is the whole reason the table exists. The grant also says what was allowed, and that is checked per request by the granted: middleware rather than by Passport’s scopes:. A token is issued to a client; a permission is given by a business, and a client several businesses connected holds one token carrying the union. Without the second check, a partner with read-only access to one shop and write access to another could write to the first — the token said so, and nothing asked whether that shop had agreed. Our own app and our own staff hold no grants: they act as the person signed in, so the token is the whole answer and granted: falls through to it. Being staff widens which businesses you reach. It does not widen what a Connect feed owns: a feed listing is refused to everyone.
Every permission in this application is registered for the web guard, and $user->can() asks the guard the request is running on. On the API that is passport, so the same account with the same permissions answered yes on the website and no here — silently, and it looked like a missing permission rather than a guard mismatch. App\Modules\Auth\Permissions::allows() asks the guard they are defined for, and both SellerScope and Actor go through it.

Where clients and grants are managed

Registers an application and decides what kind it is. That is the lock in front of every scope; registering grants nothing on its own. Revoking withdraws every token the client issued and keeps the row: tokens already out there point at it, and losing it would leave them unexplainable rather than refused. Only a client already revoked can then be deleted for good, and its tokens, codes and the sellers’ grants go with it.
Where a seller sees which applications may act for them and withdraws any of them, and where they create a key for a script they run themselves. Owning that key is the permission, so its grant row names nobody as approver.
A secret is shown exactly once in both screens. We store only the hash, so the only way back from losing it is a new key — which the screens say, because otherwise somebody will assume we can look it up.

Fields the app sees and a partner does not

App\Modules\API\Auth\InternalFields answers it for fields a partner should not even see, keyed on the client being ours. What sits behind it is not secret: a secret — a password, MFA material — is never serialised at any scope, which is a stronger guarantee than a scope check. These are things an outside integration has no business building against, because the moment they can, the field becomes a contract we have to keep.
  • descriptionTranslations on a listing, and only when you name it in fields[watches]. A translator maintains them after the fact; nobody writes them through the API. The website serves them to anyone, so there is nothing hidden here — only unpromised. translatedDescription is the one you want, and it is promised to everybody.
  • purpose on a thread. Free text with five values in practice and null for older threads, so it is analytics rather than a contract.
  • translatedBody, translationLocale, translationSourceLocale and translationPending on a message.
One pair is withheld rather than hidden: buyerEmail and buyerPhone on an order read under a seller are in every client’s document, and null for anyone but our own app (OrderSchema::buyerDetail()). They are a person’s contact details, which a shop’s people may see on its dashboard and nobody agreed to hand to a partner’s system. Add a field to that list by giving it ->hidden(fn ($request) => InternalFields::hiddenFrom($request)), not by inventing a second definition of “is this our app”.

Why the reference describes what a partner cannot reach

The published reference documents the catalogue, the pages people read, messaging, uploads, checkout and everything under /v1/me, and its OAuth flows do not offer their scopes. That is deliberate: a scope a partner can read about but never obtain is honest, while one they can ask for and are always refused reads as a bug in our API rather than in their request. And a reference that leaves out an endpoint that exists is a reference you cannot trust.