# App users & row policies

> Connect your identity provider, verify app-user tokens, configure PostgreSQL row policies, and test real requests with different users.

Source: https://docs.fortly.com/data/user-access

Use **Database → Data API → App sign-in** to connect your application’s identity provider. Then open **Objects → your table → API → Permissions** to grant operations to **Signed-in users** and configure **Row rules**. Fortly verifies access tokens and runs Data API statements as a restricted PostgreSQL role, with the user's claims scoped to that transaction.

## 1. Connect identity

The saved provider is labeled **Sign-in enabled**, **Sign-in disabled**, or **Not configured**. These labels describe the configuration. Test a real user token in the API explorer to verify the identity and grants for a request.

Choose **App sign-in** in the Data API toolbar, or **Settings → App sign-in** from a table’s API tab. Enter the exact **issuer** and **audience** from your provider's access tokens. Choose a public HTTPS **JWKS URL** for automatic signing-key refresh, or import a public JWKS object. Fortly supports **RS256**, **ES256** and **EdDSA** tokens with a signing-key `kid`. Shared-secret JWTs and private signing keys are not accepted.

Review the connection before applying it. Saving creates the **Signed-in users** API role with no grants. A verified JWT always maps to this role; a token's own `role` claim cannot select an administrator or another API role.

Keep your current provider for sign-up, login, account recovery, MFA, user management and session refresh. This integration verifies its access tokens; Fortly does not host a replacement user directory or login service. Use short-lived access tokens according to your provider's recommendations. Disabling a session at the provider does not necessarily invalidate an already-issued JWT before its expiry.

Fortly checks the signature, issuer, audience, required expiry, subject and optional `nbf`, with 15 seconds of clock leeway. A remote key set refreshes at most every 60 seconds. Key changes can take that long to become effective. Imported key sets must be updated when signing keys rotate. A signing-key fetch failure denies verification; it never falls back to accepting an unverified token.

## 2. Grant operations

Choose **Set user permissions** to return to **Permissions** with **Signed-in users** selected. Pick a table and enable only the operations it needs. Choose **Review permissions → Apply permissions**. Functions appear in the same resource list with their own **Call function** control.

Grants decide whether a request may run an operation. Row policies decide which records that operation can reach. Configure both. A signed-in user does not inherit the Public role's grants.

## 3. Protect rows

On the same table in **API → Permissions**, expand **Row rules**, enable **Row-level security**, and add a policy. Templates include **Users access their own rows**, **All rows for selected roles**, and a custom PostgreSQL expression.

For a `user_id` column containing your provider's `sub` value, the own-row template uses:

```sql
"user_id"::text = (select fortly.uid())
```

`fortly.uid()` returns the verified subject as text, which also supports providers whose user IDs are not UUIDs. `fortly.jwt()` returns the verified JSON claims. A request using only a publishable or secret API key has no app-user subject; `fortly.uid()` returns `null`.

Choose the API roles the policy applies to. Fortly resolves those choices to generated, project-specific PostgreSQL roles. Policies created outside Fortly are shown too; their existing PostgreSQL role targets are preserved when you edit them.

| Operation | Expression |
| --- | --- |
| SELECT | `USING` decides which existing rows are visible |
| INSERT | `WITH CHECK` validates the new row |
| UPDATE | `USING` selects existing rows; `WITH CHECK` validates their new values |
| DELETE | `USING` decides which existing rows may be removed |
| ALL | Both expressions can apply |

Updates generally also need a matching SELECT policy for the target rows. A permissive policy combines with other permissive policies using OR. Restrictive policies add AND conditions and also require a matching permissive policy.

Stage additions, edits, removals and RLS settings, then choose **Review changes → Apply row security**. The entire table's policy change is one transaction. An invalid expression leaves the saved configuration intact. If policies changed in another session or through SQL, reload and review them again.

> **Warning:**
>
> Enabling RLS without a matching policy closes row access for restricted API roles. Turning RLS off lets allowed API operations reach all matching rows. Review both changes before applying them.

### Owner connections

The console SQL editor, administrative table editor and direct `DATABASE_URL` use the project's owner role. Owners normally bypass RLS so you can administer your data. Public, signed-in and scoped-secret `/_data` requests use non-owner roles with no login, superuser, database ownership or RLS bypass.

**Force row-level security for owner connections** is an advanced option. It can deny console edits and application queries using the owner connection. It is not required to protect restricted Data API requests.

A granted **SECURITY DEFINER** function uses its function owner's privileges, which can bypass row policies. Review these functions' SQL and grants separately. Policies do not make privileged function code safe automatically.

## 4. Test real requests

In the table’s **API → Test request** view, or the global **Data API → API explorer**, select an endpoint and choose **Signed-in user · verified JWT**. Paste an access token from your provider. The token stays in memory and is omitted from generated examples.

```js
const response = await fetch('https://YOUR_PROJECT.fortly.com/_data/notes', {
  headers: {
    apikey: 'YOUR_PUBLISHABLE_KEY',
    Authorization: `Bearer ${userAccessToken}`,
  },
})
if (!response.ok) throw new Error(`Data API: ${response.status}`)
const rows = await response.json()
```

Repeat with another user's token. Verify that each sees their own rows, cannot write the other's rows, and cannot assign a new row to another user's ID. An invalid or expired token should return `401`; a missing operation grant should return `403`. A successful read can return an empty array when filters or row policies match no records.

The explorer supports GET, POST, PATCH, DELETE, primary-key upsert and function calls, with parameters, a filter builder, JSON bodies, response timing and generated JavaScript. Writes and function calls require an explicit review before sending. Updates and deletes need a row filter. Requests operate on your actual data.

A create-only role receives an inserted count instead of stored records. **Return only the inserted count** also requests this explicitly. Returning inserted records needs a matching SELECT policy. Upserts require both Insert and Update grants.

Canceling the wait or reaching the explorer's timeout does not guarantee a write was canceled on the server. Check the records or logs before retrying.

## Restricted database roles

Each API role gets a generated PostgreSQL role when it first runs. The role is scoped to this project's database and cannot log in or administer roles. SQL object privileges are synchronized from saved API grants; PostgreSQL also needs SELECT privileges internally to evaluate mutation filters and returned values. Gateway checks still independently deny API reads without a Select grant.

Deleting an API role revokes its keys and gateway grants. Its non-login PostgreSQL role can remain while policies refer to it; it cannot be used by new Data API requests. Project/database deletion removes the database and its generated execution roles. Project renames retain the same database identity.
