Team & security
Thirty-nine grants, four restrictions, and one real boundary
Access control in a business app has two separate jobs that are usually confused with each other. One is management: deciding what a member should be shown so they can do their work without wading through everything else. The other is security: making certain one business's data can never reach another's. Akkad treats these as different problems with different guarantees, and says plainly which is which.
A member's access is a list of grants. Each grant is a single, named capability — not a role you have to reverse-engineer, and not a tier where “manager” silently includes eleven things nobody enumerated. You tick what someone does and that is what they can do.
Owners are the exception, and the exception is total: an owner bypasses every check on the platform. That is deliberate. There is always one person who can get to everything, and pretending otherwise produces businesses locked out of their own data by a permission model that was too clever.
01
Thirty-nine grants, in the groups you actually see
These are the eight groups the app presents, with every grant inside them. Nothing is bundled — each name below is a separate switch.
| Group | Grants |
|---|---|
| Orders (7) | createOrders · editOrders · deleteOrders · printOrders · sendShipments · viewShipmentLog · accessCashier |
| Items (4) | createItems · editItems · deleteItems · viewCostPrice |
| Performance & statistics (8) | viewDashboardStatistics · viewSalesPerformance · viewInventoryPerformance · viewUsersPerformance · viewCategoriesPerformance · viewExpiryReport · viewStockHealth · viewCitiesPerformance |
| Finance (6) | manageExpenses · manageIncomes · manageCashbox · manageCashier · manageDebts · viewFinancialReport |
| People & access (4) | manageUsers · manageCustomers · manageSuppliers · managePurchaseOrders |
| Platform settings (7) | manageSettings · manageCategories · managePrinting · managePackageSize · manageIntegration · manageShippingCostRules · manageDiscountRules |
| Data & export (2) | exportOrders · exportItems |
| Online presence (1) | manageOnlinePresence |
viewCostPrice is worth calling out separately. Cost price is the number most businesses least want circulating, so it is not implied by being able to edit items — it is its own grant, and a member can maintain the catalogue all day without ever seeing what you paid. Grants renamed over the platform's life are migrated automatically, so an older account keeps working without anyone re-ticking boxes.
02
The unusual part
Restrictions that limit rather than allow
Grants say what a member may do. Restrictions say what a member may see, and they run the opposite way round from everything else in the interface: adding an entry makes a member's world smaller, not larger. All four live behind a single “Advanced” row that shows an n / 4 counter, so it is always visible at a glance whether a member is scoped at all. They are hidden entirely for owners, who can never be restricted.
No entry means unrestricted
A member with no restriction recorded sees everything — including categories and colleagues added next month. This is the property that matters most in practice: an unrestricted member does not need re-editing every time the business grows.
An empty list means nothing
A recorded restriction holding no selections is a real instruction, and it means the member sees none of that thing. Empty is not the same as absent, and conflating them would be the difference between “sees everything” and “sees nothing”.
Selecting everything removes the restriction
Tick every category and the entry is deleted rather than stored as a list of all current categories. Otherwise a member who was granted the whole catalogue today would silently stop seeing whatever was added tomorrow.
03
The four restrictions
Two govern what a member can change; two govern what a member can see at all.
| Restriction | Effect when present |
|---|---|
| Cannot edit finalised orders | Once an order's saved status is shipped or delivered, this member can no longer change its contents. They can still read it. |
| Cannot set finalised statuses | Shipped and delivered are removed from this member's manual status picker. The carrier integration can still set them — the point is to stop a member marking an order delivered by hand, not to stop the courier reporting reality. |
| Category visibility | The member sees only the listed top-level categories, and the items inside them. A subcategory is visible when its parent is allowed. The scope rides every read path — list, search, export, images, pack sizes and batches — and it also governs categories, preload and websites. |
| Order-creator visibility | The member sees only orders written by the listed people. The options are every member of the organisation plus a storefront pseudo-creator covering public storefront and landing-page orders, so “their own orders plus everything the website brings in” is expressible. |
Hidden records answer 404, not 403
A 403 confirms that the thing exists and you are not allowed to see it. A 404 says nothing at all. Items and orders outside a member's scope answer as though they were never there — on read, on edit, and on appending to an order.
The bulk endpoint is filtered too
Resolving up to four hundred codes in one call is genuinely useful for stocktakes. It is also the ideal shape for a barcode-enumeration probe, which is precisely why the category filter is applied to it. Without that, the one endpoint built for volume would have been the one way around the restriction.
A restricted member is not even notified
Push notification for a new order respects order-creator visibility. Someone who cannot see an order does not receive an alert about it — otherwise the notification itself would tell them what the orders list is hiding.
Filtering happens before counting
Visibility is applied ahead of every other filter and ahead of the count, so page totals and pagination reflect what the member can see. A list that says “87 orders” and shows 41 is a bug in the making; this avoids it structurally. Bulk shipping sweeps are scoped the same way, so a restricted member cannot dispatch the whole organisation in one action.
Restrictions are a management convenience, not a security boundary
This is worth being blunt about. Visibility restrictions are designed to keep a member's screen relevant and to reduce the blast radius of ordinary mistakes. They are not designed to withstand someone determined to defeat them, and if their stored data is malformed they fail open — the member sees more, not less, because the alternative is a business that silently cannot work. The real boundary, the one built to hold, is organisation scoping. Do not use a category restriction where you mean a separate organisation.
05
The boundary that does hold
Every business endpoint on the platform is addressed by organisation, and every one of them passes through the same check before any work happens: does this organisation exist, is this user a member of this organisation, and does this user hold the permission this endpoint requires. There is no path that skips it and no endpoint that assumes membership from context.
The two failure modes are deliberately distinct. A missing organisation or user is a 404. A real organisation you are not a member of, or a permission you do not hold, is a 403 naming which of the two it was. That distinction is for the person debugging their own setup — it never reveals anything about a business you have no relationship with.
The bulk-sync endpoint that feeds offline mode validates membership explicitly rather than inheriting it. That endpoint returns an entire organisation's catalogue in one response, and it was worth writing the check twice rather than discovering that any authenticated user could pull down anyone's snapshot.
06
Sessions and sign-in
Bearer tokens, nothing else
Authentication is a signed token carrying the user id, sent as a bearer token. There is no cookie session and no alternative path to authenticate a request. Tokens are held in the device's secure storage on the client rather than in ordinary app storage.
Long-lived on purpose
Access tokens last six months. A shop floor where staff are signed out mid-shift produces workarounds — shared logins, written-down passwords — that are worse than the risk the short expiry was guarding against.
Social sign-in verified properly
Apple Sign-In tokens are verified against Apple's own public keys, cached for twenty-four hours. Google and Apple both run through brokered flows where the confidential secret never leaves the server.
07
Rate limits, by endpoint
Limits are keyed on the real client address, proxy-aware, so a shared gateway does not make one customer's traffic look like an attack.
| Endpoint group | Limit |
|---|---|
| Authentication | 5 per minute and 20 per hour |
| Push registration / deletion | 10 per minute |
| Push sending | 50 per hour |
| Bulk import | 8 per day |
| Bulk image URLs | 5 per day |
| Item image generation | 25 per day, keyed per organisation |
| Client diagnostics | 12 per hour |
| News | 5 per hour |
| Announcements | 20 per hour and 50 per hour |
| Public storefront | 500 per hour, keyed per address and store |
| Storefront checkout | 20 per hour |
| Storefront browsing | 30 to 60 per minute |
| Public order creation | 20 per hour |
| Payments | 30 per minute |
| Discount quote | 60 per minute |
| Promo code validation | 30 per minute |
The storefront limits are keyed per address and per store rather than globally, so a busy shop's shoppers are never throttled by another shop's traffic.
08
Underneath every request
The layer nobody should ever have to think about, listed here because it is reasonable to ask what it consists of.
Request size caps
Any request declaring more than 30 MB is rejected outright. The multipart parser independently caps both file size and part size at 30 MB, so the limit cannot be sidestepped by splitting an upload into parts.
Query hardening
Any API request asking for more than 100 records is rejected, as is any query string longer than a thousand characters. Both are limits that legitimate use never reaches and scraping tools reach immediately.
Probes are recorded, not just refused
Requests for the usual targets — an admin panel, a WordPress path, an environment file, a config file — return 404 and are written as security events. Refusing them is the minimum; knowing when they started is the useful part.
Security headers on every response
Content-type sniffing off, framing denied, a strict referrer policy, and a server identity header that reports Akkad rather than the real stack. The last one is not security by itself; it removes a free hint.
Per-request identifiers
Every request carries an identifier that is echoed back to the caller and threaded through structured logging. When something goes wrong you can point at one specific request rather than describing roughly when it happened.
Retention with an end date
Monitoring events are kept thirty days and request logs fourteen. Organisations inactive for twelve months are warned and then deleted. Data that is no longer needed is a liability, and the cleanup runs on a schedule rather than on good intentions.
09
Questions
If I restrict a member to two categories, do they get new categories automatically?
No — that is the point of the inverse semantics. A member with a restriction sees only what is listed, and a category created later is not listed. A member with no restriction at all inherits everything new automatically. If you want someone to keep seeing the whole catalogue as it grows, leave them unrestricted rather than ticking every box.
What is the difference between an empty restriction and no restriction?
No entry means unrestricted — the member sees everything, now and later. An empty list is an explicit instruction that the member sees nothing of that kind. They look similar in a database and behave as opposites, so the interface keeps them distinct.
Why does a hidden order return “not found” rather than “not allowed”?
Because “not allowed” confirms it exists. If a member can learn that order 4417 is real simply by being refused it, the restriction has leaked the thing it was hiding. Hidden records behave as though they were never created.
Can I use category restrictions to separate two businesses I own?
No. Restrictions are a management convenience and are documented as failing open if their data is malformed. Separate businesses should be separate organisations, which is the boundary that is actually enforced on every request.
Will a restricted member get a notification about an order they cannot see?
No. Order-creator visibility is applied to push notifications as well as to the orders list, so the alert cannot reveal what the list is hiding.
Can an owner be restricted?
No. Owners bypass every permission check, and the restriction controls are hidden for them entirely. Someone always has to be able to reach everything.
Related
Reports & analytics
Nine reports, each behind its own grant — including the team leaderboard that honours creator visibility.
Orders
The lifecycle these restrictions govern: statuses, editing, change logs and idempotent creation.
Apps & offline
Six platform targets, offline order taking, printing and the notification system permissions gate.
آن را روی فهرست کالاهای خودتان ببینید
با بستهٔ مناسب شروع کنید، کالاهایتان را از یک فایل اکسل وارد کنید، و همان روز فروشگاه، صندوق و دفتر سفارشِ فعال داشته باشید.
بستهٔ رایگان موجود است. برای امتحان به کارت نیاز نیست. ورود اطلاعات، فهرست کالاهای فعلی شما را همراهتان میآورد.