OwnCRM
Exhibit · access control

Who sees what.

Salesforce's sharing model is one of the hardest things to leave — profiles, permission sets, org-wide defaults, a role hierarchy, sharing rules. OpenForce doesn't approximate it. It rebuilds it in two layers you own: application roles in your repo, and row-level-security policies in your PostgreSQL database — so the database itself enforces who sees which rows.

This is our strongest-built surface. Everything below is generated by the engine today; where a piece is transcribed but not yet fully wired, the honesty panel says so plainly.

§ 01The Salesforce model — in one ledgerof 04

Before rebuilding it, we inventory the whole thing. This is every construct that decides visibility in a Salesforce org — the X-Ray reads all of it out of your metadata.

  • §Profiles, permission sets & permission-set groups — object CRUD and field-level security, granted per user.
  • §Org-wide defaults (OWD) — the baseline for each object: Private, Public Read, Public Read/Write, or Controlled by Parent.
  • §Role hierarchy — managers inherit visibility of records owned by people below them.
  • §Sharing rules — ownership-based and criteria-based grants that open records above the OWD baseline.
  • §Manual shares & teams — ad-hoc record shares and account/opportunity teams.
  • §Public groups & queues — named principals that sharing rules and ownership resolve against.
  • §Sharing sets — the portal/Experience-Cloud construct that scopes an external user to records related to their own account or contact.
§ 02The two-layer rebuildof 04

Salesforce keeps sharing in the platform layer only. We put it in two layers — the application and the database — so a bug in one is not a data breach. Here is where each Salesforce construct lands.

Layer 1 — application RBAC, in your repo

Salesforce constructRebuilt as
Permission setA role in a generated rbac module.
Object CRUD, checked on the routeEvery generated API route calls can(role, action, object) against the verified JWT role — not a client header.
Permission-set groupA composite role: the union of its members, minus any muting permission set.
System AdministratorA synthesized app-level admin role that short-circuits the checks — never transcribed from an org profile.
Field-level securityTranscribed per field, per role (read / write / none), enforceable via can(role, action, object, field). See the honest edge below.

Layer 2 — the database itself, as PostgreSQL row-level security

Salesforce constructRebuilt as a PostgreSQL policy
Org-wide default = PrivateOwner-keyed policy: a row is visible when its owner column equals the current user.
Role hierarchyRecursive subordinate resolution: a precomputed ancestor/descendant closure table, tested with an EXISTS clause.
Criteria sharing ruleA SQL predicate; a lookup criterion maps to the foreign-key column (Region__cregionId).
Controlled by ParentA parent-visibility read join — you can read the child when you can see its parent.
Public groups & queuesSharing principals in the policy; their membership is seeded as a migration step.
Per-request identityTransaction-local session context (set_config('app.user_id', …, true)) set on every query, so the database enforces row visibility even if the app is bypassed.
§ 03A real policy — from a generated rls-policies.sqlof 04

This is not an illustration. It is a trimmed policy from the rls-policies.sql the engine emitted for a real Program Management Module app — a Private object, so a row is visible only to its owner, to anyone above the owner in the role hierarchy, or to an admin.

POSTGRESQL rls-policies.sql · Bucket__mdt (OWD: Private)

-- Bucket__mdt -> "Bucket__mdt"  (OWD: Private)
ALTER TABLE "Bucket__mdt" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "Bucket__mdt" FORCE ROW LEVEL SECURITY;

CREATE POLICY "Bucket__mdt_select_policy" ON "Bucket__mdt"
  FOR SELECT USING (
    current_setting('app.is_admin', true) = 'true'
    OR "Bucket__mdt"."ownerId" = current_setting('app.user_id', true)
    OR EXISTS (SELECT 1 FROM "_role_hierarchy" rh
               WHERE rh."ancestor_role"   = (SELECT "role" FROM "AppUser" WHERE "id" = current_setting('app.user_id', true))
                 AND rh."descendant_role" = (SELECT "role" FROM "AppUser" WHERE "id" = "Bucket__mdt"."ownerId"))
  );

FORCE ROW LEVEL SECURITY means the policy applies even to the table owner. The identity comes from current_setting('app.user_id'), which the generated app sets transaction-locally on every query from a verified JWT — so this rule holds at the database boundary, not just in application code. The _role_hierarchy lookup is the recursive-subordinate resolution, precomputed into a closure table at generation time.

§ 04The honest edges — verified todayof 04

The brand is engineering honesty, so here is exactly where this surface stops today. Everything above is real; these four lines are the truth about the edges.

Object CRUD is enforced everywhere; field-level enforcement on the route is not yet wired. Every generated route checks can(role, action, object), and field-level security is transcribed per field, per role, with a can(role, action, object, field) primitive ready. But the generated route handlers do not yet call it per field — so today a role with object read gets all columns. Field enforcement at the route is on the build list; we do not claim it as done.

Database-level enforcement requires running as a least-privilege role. Row-level security only enforces when the app connects as a non-superuser role without BYPASSRLS. The deploy bundle warns loudly until the operator switches the app's database role; demos run as superuser, where the policies are set but bypassed. Real row security is one role switch away, and the generator tells you so.

Sharing sets (portal users) are inventoried, not yet auto-translated. The record-scoping engine is built and fail-closed; the automatic translation of a Salesforce sharing set into a relationship-keyed policy is on our roadmap. Today the X-Ray flags it as not-yet-pipelined, and an engineer wires those portal rules onto a database that already speaks that language.

Manual shares and public groups/queues are inventoried and flagged, not silently dropped. Ad-hoc record shares are surfaced in the emitted policy file with migration guidance; group and queue membership is a seed-time data migration, not something Salesforce exposes in metadata to transcribe.

Your permission model is not a Salesforce feature — it's rows in your database and code in your repo.