Build my booking setup

Security

Every organization walled off. By design, not by discipline.

No developer has to remember to filter a query by organization. A global scope does it automatically, on every model that needs it, so a stale or guessed id for someone else's data resolves to nothing, not a leak.

  • 8 models hardened A dedicated audit found pricing, availability and tax rules reachable by guessing an id, and closed it for good
  • 5 attempts a minute Login, password reset and registration are throttled per email and per IP, blocking brute force and enumeration
  • Rechecked, not cached Every sensitive action re-verifies its permission live against the database, not a role cached at login

Four layers, not one promise.

Each one closes a different way a multi-tenant platform usually leaks, quietly, until someone goes looking.

01

Automatic isolation, not manual discipline

A global scope filters every query by organization the moment a model boots, with nothing for a developer to remember and no single query that can forget it, including the ones behind route links.

02

A suspended organization loses access immediately

Admin access gets checked on every request, not just at login, so suspending an organization locks its team out on their very next click. Its products stop being publicly reachable at the same moment.

03

Destructive actions, checked twice

Voiding a ticket or cancelling an order requires its specific permission rechecked at the exact moment of the action, independent of whatever permission got someone onto the screen in the first place.

04

What gate staff and the public API can never see

Cost and margin are never written into a storefront or gate-scan response. It isn't hidden behind a flag that could be misconfigured later; the field simply never exists in what gets returned.

How it holds

How a guessed id, a revoked permission or a stale link all fail safely

Five mechanisms, each closing a different way isolation usually breaks under real use.

  1. 1

    Every query carries its own filter, invisibly

    A global scope adds the organization filter to every query issued on a model that carries it, applied the moment the model boots, with no filter to remember and no way for a single query to skip it.

  2. 2

    A stale id resolves to nothing, not someone else's data

    Route links rely on that same scope, so a link to another organization's record resolves to a plain 404 before any handler code even runs, never a permission error that would confirm the record exists.

  3. 3

    Writes get re-validated too, not just reads

    The scope alone protects reads and deletes, but not a blind write to a foreign id, so an id going into a pivot table or a foreign key column gets re-resolved through its own target's scoped query first.

  4. 4

    A permission gets rechecked at the moment it matters

    Every sensitive action re-runs a live permission check against the database when it actually happens, not once at page load, so a permission revoked mid-session stops working on the very next click, not the next login.

  5. 5

    The database backs up the application layer

    Deleting a product, option or ticket category with real sales gets refused twice: the application layer explains why before even attempting it, and the database's own foreign key is declared to block that same delete, so a path that skipped the check would still fail.

The detail, as it is

What a gate scan can never return

The response a scanner sees is a hand-written list of exactly what gate staff needs. Cost, net price and margin were never added to it, so there is nothing to accidentally expose later.

GET /api/v1/tickets/lookup
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
{
  "returned_to_gate_staff": {
    "ticket_code": "AB12CD34EF",
    "status": "redeemed",
    "product_name": "City Food Tour",
    "buyer_name": "Jane Buyer",
    "position": 2,
    "quantity": 3
  },
  "never_included": [
    "cost_amount",
    "net_amount",
    "margin"
  ]
}
Excluded by design, not by flag

The detail almost nobody explains

The vulnerability that got closed before it shipped wide

An audit found eight models reachable by guessing an id from another organization: six governing pricing and availability rules, two governing tax assignments. None carried their own organization filter; each inherited one from a parent relation that a raw lookup by a client-supplied id skipped entirely. Every one of them now carries its own organization id, closing that exact class of gap for good, and a dedicated test suite keeps it closed by attempting precisely that guess, on every one of the eight, every time the suite runs.

Frequently asked questions

No. Every model that needs it carries a global scope filtering by organization automatically, which also protects route links: a stale or guessed id for another organization's record resolves to a 404, never a permission error that would confirm the record exists.

Its admin team loses panel access on their very next request, not just their next login, and its products stop being reachable through the public API at the same moment. Suspension is enforced, not just recorded.

Yes. Every sensitive action re-checks the permission live against the database at the moment it runs, not a role cached when the page first loaded. The very next click after a revocation gets blocked.

No, twice over. The application layer refuses the delete with a clear explanation before even attempting it, and the database's own foreign key is declared to restrict that same delete, so even a path that bypassed the application check would still fail.

No. Those responses are hand-written to include only what the recipient actually needs. The field is never written into the response in the first place, not hidden behind a flag that could be misconfigured.

More bookings. More channels. The same calm.

Tiqory keeps availability, pricing, payments and access working together as your operation grows. Tell us where you are headed; we’ll show you how to get there without starting over.