Send consistent updates through the PDP
Route data writes, such as creating a user or assigning a role, through your policy decision point (PDP), so a permission check right after the write sees the new data (read-your-own-writes). This page is for developers whose application creates data and checks permissions on that data in the same flow. The feature is also called proxy facts, and the PDP endpoints are the Local Facts API.
How proxy facts work
By default, the SDK sends data writes to the Permit API, and the Permit control plane syncs the change to your PDPs a short time later. A check that runs before the sync finishes can return a decision based on old data.
With proxy facts, the SDK sends the write to the PDP. The PDP forwards the write to the Permit API, and then waits until the PDP receives the data update before it responds. The PDP waits up to a timeout, and then responds according to the timeout policy.
Prerequisites
- A PDP container, version
0.5.1or later, that your application can reach. See Run the PDP. Thetimeout_policyoption requires PDP version0.8.0or later. - A Permit SDK configured with your environment API key (Get your API key)
Create a user, then check permissions
Turn on proxy facts in the SDK configuration, and point the SDK at your PDP. In the following examples, replace <your-api-key> or <YOUR_API_KEY> with your environment API key.
- Python
- Node.js
- Go
- Java
from permit import Permit
# Initialize SDK with proxy_facts_via_pdp enabled
permit = Permit(
token="<your-api-key>",
pdp="http://localhost:7766",
proxy_facts_via_pdp=True
)
async def create_and_check():
# Create user
user = await permit.api.users.create({
"key": "user123",
"email": "user@example.com"
})
# Check for permissions right after
allowed = await permit.check(user.key, "read", "document")
print(f"Permission granted: {allowed}")
With proxy_facts_via_pdp enabled, permit.api.users.create() returns after the PDP receives the user data, so the permission check that follows sees the user.
import { Permit } from "permitio";
// Initialize SDK with proxy_facts_via_pdp enabled
const permit = new Permit({
token: "<your-api-key>",
pdp: "http://localhost:7766",
proxyFactsViaPdp: true
});
async function createAndCheck() {
// Create user
const user = await permit.api.users.create({
key: "user123",
email: "user@example.com"
});
// Check for permissions right after
const allowed = await permit.check(user.key, "read", "document");
console.log(`Permission granted: ${allowed}`);
}
With proxyFactsViaPdp enabled, permit.api.users.create() returns after the PDP receives the user data, so the permission check that follows sees the user.
package main
import (
"context"
"fmt"
"github.com/permitio/permit-golang/pkg/config"
"github.com/permitio/permit-golang/pkg/enforcement"
"github.com/permitio/permit-golang/pkg/models"
"github.com/permitio/permit-golang/pkg/permit"
)
func main() {
ctx := context.Background()
// Initialize SDK with proxy_facts_via_pdp enabled
permitConfig := config.NewConfigBuilder("<YOUR_API_KEY>").
WithPdpUrl("http://localhost:7766").
WithProxyFactsViaPDP(true).
Build()
permitClient := permit.NewPermit(permitConfig)
// Create user
newUser := models.NewUserCreate("user123")
newUser.SetEmail("user@example.com")
user, _ := permitClient.Api.Users.Create(ctx, *newUser)
// Check for permissions right after
allowed, _ := permitClient.Check(
enforcement.UserBuilder(user.Key).Build(),
"read",
enforcement.ResourceBuilder("document").Build(),
)
fmt.Printf("Permission granted: %v\n", allowed)
}
With proxy facts enabled in the configuration, the create call returns after the PDP receives the user data, so the permission check that follows sees the user.
import io.permit.sdk.Permit;
import io.permit.sdk.PermitConfig;
import io.permit.sdk.enforcement.Resource;
import io.permit.sdk.enforcement.User;
import io.permit.sdk.openapi.models.UserCreate;
public class Example {
public static void main(String[] args) throws Exception {
// Initialize SDK with proxy_facts_via_pdp enabled
PermitConfig config = new PermitConfig.Builder("<your-api-key>")
.withPdpAddress("http://localhost:7766")
.withProxyFactsViaPdp(true)
.build();
Permit permit = new Permit(config);
// Create user
UserCreate userCreate = new UserCreate("user123")
.withEmail("user@example.com");
permit.api.users.create(userCreate);
// Check for permissions right after
boolean allowed = permit.check(User.fromString("user123"), "read", Resource.fromString("document"));
System.out.println("Permission granted: " + allowed);
}
}
With withProxyFactsViaPdp(true), the create call returns after the PDP receives the user data, so the permission check that follows sees the user.
Verify the consistent update
Run the example for a user key that doesn't exist yet, with a policy that allows the action for the user. The first permission check after the create call returns true. When a write times out with the fail timeout policy, the PDP responds with HTTP status 424.
Configuration options
| Option | Values | PDP default | Description |
|---|---|---|---|
| Timeout | 0 | Don't wait. The PDP responds right after it forwards the write. | |
A positive number, such as 10 | 10 | Wait up to this number of seconds for the data update. | |
A negative number, such as -1 | Wait with no time limit. | ||
| Timeout policy | ignore | ignore | When the timeout passes, respond with the result of the write. |
fail | When the timeout passes, respond with HTTP status 424 Failed Dependency. Requires PDP 0.8.0 or later. |
Set the options at three levels. A level lower in the list overrides the levels above it:
- PDP environment variables, the defaults for every request
- SDK configuration, for every write from the SDK client
- Per-operation settings, for one write
PDP configuration
| Environment variable | Default | Description |
|---|---|---|
PDP_LOCAL_FACTS_WAIT_TIMEOUT | 10 | Default timeout in seconds |
PDP_LOCAL_FACTS_TIMEOUT_POLICY | ignore | Default timeout policy: ignore or fail |
SDK configuration
In the Python and Node.js SDKs, when you don't set a timeout or a timeout policy, the SDK sends no value and the PDP defaults apply.
- Python
- Node.js
- Go
- Java
# SDK-level configuration (applies to all operations)
permit = Permit(
token="<your-api-key>",
pdp="http://localhost:7766",
proxy_facts_via_pdp=True,
facts_sync_timeout=10, # Optional: Uses PDP default if not specified
facts_sync_timeout_policy="ignore" # Optional: Uses PDP default if not specified
)
# All operations will use the SDK-level settings
# user = await permit.api.users.create(user_data) # inside an async function
// SDK-level configuration (applies to all operations)
const permit = new Permit({
token: "<your-api-key>",
pdp: "http://localhost:7766",
proxyFactsViaPdp: true,
factsSyncTimeout: 10, // Optional: Uses PDP default if not specified
factsSyncTimeoutPolicy: "ignore" // Optional: Uses PDP default if not specified
});
// All operations will use the SDK-level settings
const user = await permit.api.users.create({ /* user data */ });
// SDK-level configuration (applies to all operations)
permitConfig := config.NewConfigBuilder("<YOUR_API_KEY>").
WithPdpUrl("http://localhost:7766").
WithProxyFactsViaPDP(true).
WithFactsSyncTimeout(10 * time.Second). // Optional: defaults to the SDK default timeout
Build()
permitClient := permit.NewPermit(permitConfig)
// All operations will use the SDK-level settings
user, _ := permitClient.Api.Users.Create(ctx, *newUser)
// SDK-level configuration (applies to all operations)
PermitConfig config = new PermitConfig.Builder("<your-api-key>")
.withPdpAddress("http://localhost:7766")
.withProxyFactsViaPdp(true)
.withFactsSyncTimeout(10) // Optional: Uses PDP default if not specified
.withFactsSyncTimeoutPolicy("ignore") // Optional: Uses PDP default if not specified
.build();
Permit permit = new Permit(config);
// All operations will use the SDK-level settings
permit.api.users.create(userCreate);
Operation-specific configuration
Override the timeout for one write. In Python, wait_for_sync() is a context manager that returns a client with the timeout. In Node.js, waitForSync() returns an API client with the timeout, and also accepts a timeout policy as a second argument.
- Python
- Node.js
- Go
# SDK initialization with proxy_facts_via_pdp enabled
permit = Permit(
token="<your-api-key>",
pdp="http://localhost:7766",
proxy_facts_via_pdp=True
)
# Override the default timeout for a specific operation
async def create_user_with_timeout(user_data: dict):
with permit.wait_for_sync(timeout=15) as p:
return await p.api.users.create(user_data)
// SDK initialization with proxy_facts_via_pdp enabled
const permit = new Permit({
token: "<your-api-key>",
pdp: "http://localhost:7766",
proxyFactsViaPdp: true
});
// Override the default timeout for a specific operation
const user = await permit.api.users.waitForSync(15).create({ /* user data */ });
// SDK initialization with proxy_facts_via_pdp enabled
permitConfig := config.NewConfigBuilder("<YOUR_API_KEY>").
WithPdpUrl("http://localhost:7766").
WithProxyFactsViaPDP(true).
Build()
permitClient := permit.NewPermit(permitConfig)
// Override the default timeout for a specific operation
timeout := 15 * time.Second
user, _ := permitClient.Api.Users.WaitForSync(&timeout, api.WaitForSyncOptions{}).Create(ctx, *newUser)
Call the Local Facts API directly
To use proxy facts without an SDK, send facts requests to the PDP instead of the Permit API. The routes and request bodies are the same as the Permit API facts routes, without the /v2 prefix and without the project and environment in the path:
| Permit API | PDP Local Facts API |
|---|---|
https://api.permit.io/v2/facts/{proj}/{env}/... | http://localhost:7766/facts/... |
Set the timeout and the timeout policy for a request with headers:
POST /facts/...
Headers:
X-Wait-timeout: 10
X-Timeout-policy: ignore
HTTP header names are case-insensitive, so X-Wait-timeout and X-Wait-Timeout are the same header.
The following two requests create the same user. The first request goes through the PDP, and the second goes to the Permit API:
curl -X POST http://localhost:7766/facts/users \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"key": "user123",
"email": "user@example.com"
}'
curl -X POST https://api.permit.io/v2/facts/default/prod/users \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"key": "user123",
"email": "user@example.com"
}'
Supported APIs
The PDP waits for the data update on these routes:
# Users
POST /facts/users
PUT /facts/users/{user_id}
PATCH /facts/users/{user_id}
# Tenants
POST /facts/tenants
# Role Assignments
POST /facts/users/{user_id}/roles
POST /facts/role_assignments
# Resource Instances
POST /facts/resource_instances
PATCH /facts/resource_instances/{instance_id}
# Relationship Tuples
POST /facts/relationship_tuples
The PDP also waits on DELETE /facts/users/{user_id}/roles and DELETE /facts/role_assignments, which remove role assignments.
The PDP forwards requests to other facts routes to the Permit API without waiting for the data update.
The PDP doesn't wait for the role_assignments field of a user request (POST /facts/users, PUT /facts/users/{user_id}, PATCH /facts/users/{user_id}). A permission check right after the user request can miss those roles. Send each role assignment in a separate role assignment request after you create or update the user.
For API or SDK support, ask in the Permit Slack community.
Best practices
Performance considerations
- Write latency: a write through the PDP takes longer than a write to the Permit API, because the PDP waits for the data update.
- Unsupported routes: requests to routes the PDP doesn't wait on still pass through the PDP to the Permit API, which adds a network hop.
Deployment recommendations
Proxy facts guarantee the update only on the PDP that handled the write. Send the write and the following permission checks to the same PDP instance.
- Recommended: deploy a centralized PDP or a PDP sidecar next to your application. The same PDP instance handles the write and the check.
- Less reliable: with a load-balanced cluster of PDPs, a check can go to a PDP that hasn't received the data update yet.