Security
The eleven rules Postgres enforces, in plain language.
The app checks permissions so the interface makes sense. The database checks them again so they actually hold. Each rule below is enforced by Postgres (row-level security policies, column grants, triggers or check constraints) and has an RLS test that tries to break it, including writes that must fail.
These hold in local mode and anywhere else Portline runs on its database. Demo mode has no database; it applies the same rules in the app.
- Your plan is not yours to edit. Plan, status, seats and period end can only be written by the service role. An owner running
update pl_subscriptions set plan = 'scale'gets an error. - Locked areas show nothing. If the plan doesn't include an area, its rows don't come back. A Starter workspace reads zero dock appointments, not an error page with data behind it.
- Viewers write nothing. Every insert and update checks for the edit level. A viewer inserting a dock appointment is refused.
- Document reviews need approval rights, and are signed. Only admins and the owner can verify or reject a document, and the database stamps the review with the caller's id. A dispatcher setting a document to verified fails; when an admin does it,
reviewed_byis set to that admin no matter what was sent. - Notifications can only be marked read. You can set
read_aton your own notifications and nothing else. Rewriting a notification's title or link is refused. - The audit trail can't be forged or edited. Audit rows are written as the caller only, and nobody can update or delete them.
- Nobody promotes themself. You can't change your own role, nobody becomes owner through the app, and admins can't touch the owner. A dispatcher can't change anyone's role.
- Workspaces are isolated. Every policy keys on membership. A member of one workspace reads zero rows of another.
- A lapsed workspace is read-only. Edits need an
active,trialingorgracesubscription. After lapse every write fails. - A driver can't check out before checking in. A check constraint requires a check-in time before a check-out time, and the check-out after it.
- Messages are posted as you. The author of a message must be the caller. Posting as someone else fails.
In the app as well
- Every server action validates its input and re-checks the permission level before touching data.
- Redirects after sign-in only go to paths on the same site, so a crafted
?next=link can't send you elsewhere. - Any outbound URL a user could supply (webhooks, carrier endpoints) goes through a guard that refuses non-https, private and internal addresses.
- Demo mode refuses to start in production and makes no outbound requests.