Exposure Profiles
Exposure Profiles define what data a user shares with apps. They're the key to user-controlled privacy.
What is an Exposure Profile?
An exposure profile is a set of rules that specify:
- What types of nodes are accessible
- Which tags are included or excluded
- What permissions are granted (read, write, delete)
Think of it as a privacy filter between user data and any app outside of the first-party app (L0). Both native apps (L1) and third-party apps (L2) operate through exposure profiles.
How It Works
┌─────────────────────────────────────────────────────────────────┐
│ USER'S DATA VAULT │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ All Data: Notes, Contacts, Personal, Health... │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ Exposure Profile Filter │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Filtered: Only Work notes, Only tag "public" │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────┐
│ L1 / L2 App │
└─────────────────┘
Profile Structure
{
"id": "profile_work123",
"name": "Work Data Only",
"description": "Share only work-related notes and documents",
"owner_id": "user_123",
"is_default": false,
"tag_permissions": {
"discover": { "tag_ids": ["tag_work"], "allow_all": false },
"read": { "tag_ids": ["tag_work"], "allow_all": false },
"propose": { "allow_all": false },
"edit": { "allow_all": false },
"create": { "allow_all": false }
},
"schema_permissions": {
"read": { "schema_ids": ["schema-uuid"], "allow_all": false },
"write": { "schema_ids": [], "allow_all": false }
},
"tag_filters": {
"include_all": [],
"include_any": ["work"],
"exclude_any": ["private", "personal"]
},
"allowed_node_types": ["EXPERIENCE", "BELIEF"],
"max_detail_level": "full",
"created_at": "2024-01-15T10:30:00Z"
}
Granular Tag Permissions
Tags are the atomic unit of privacy control. Each permission level controls what apps can do with nodes that have specific tags:
| Permission | Description |
|---|---|
discover | See that tags exist (names only, no node content) |
read | Read nodes within those tags |
propose | Suggest new nodes via proposals (requires user approval) |
edit | Modify existing nodes directly |
create | Auto-create nodes (no approval needed) |
Each permission specifies either allow_all: true (unrestricted) or a list of tag_ids the permission applies to.
Schema Permissions
Control which schemas an app can read or write nodes against:
| Permission | Description |
|---|---|
read | Read nodes that use specific schemas |
write | Create or modify nodes using specific schemas |
Each permission specifies either allow_all: true (unrestricted) or a list of schema_ids the permission applies to.
Use the special sentinel "__freeform__" in schema_ids to include nodes that have no schema (schema_id IS NULL).
{
"schema_permissions": {
"read": { "schema_ids": ["schema-uuid-1", "__freeform__"], "allow_all": false },
"write": { "schema_ids": ["schema-uuid-1"], "allow_all": false }
}
}
Tag Filters
For filtering which nodes are visible, profiles support:
| Filter | Description |
|---|---|
include_all | Nodes must have ALL these tags |
include_any | Nodes must have AT LEAST ONE of these tags |
exclude_any | Nodes must NOT have ANY of these tags |
Advanced boolean logic is available via tag_expression:
{
"tag_expression": {
"op": "AND",
"conditions": [
{"tag": "work"},
{"op": "OR", "conditions": [{"tag": "urgent"}, {"tag": "today"}]}
]
}
}
Additional Filters
| Field | Description |
|---|---|
allowed_node_types | Only these node types are visible |
excluded_node_types | These node types are hidden |
allowed_node_ids | Specific node UUIDs to share |
date_range_start / date_range_end | Only nodes within date range |
max_detail_level | full, summary, or minimal |
transformations | Data transformations (e.g., ["redact_pii"]) |
Working with Exposure Profiles
Exposure profiles are managed by users on app.ofself.ai (L0). Native apps (L1) and third-party apps (L2) cannot create or modify profiles directly — they operate within the permissions the user has configured.
Create a Profile
curl -X POST "https://api.ofself.ai/api/v1/exposure-profiles" \
-H "Authorization: Bearer your-jwt-token" \
-H "Content-Type: application/json" \
-d '{
"name": "Work Apps Profile",
"description": "For productivity apps",
"tag_permissions": {
"discover": { "allow_all": true },
"read": { "tag_ids": ["work-tag-id"], "allow_all": false },
"propose": { "allow_all": false },
"edit": { "allow_all": false },
"create": { "allow_all": false }
},
}'
Create from Template
curl -X POST "https://api.ofself.ai/api/v1/exposure-profiles/from-template" \
-H "Authorization: Bearer your-jwt-token" \
-H "Content-Type: application/json" \
-d '{
"template": "restrictive",
"name": "Limited Access Profile"
}'
Templates: transparent (full access) or restrictive (minimal access).
Get Default Profile
curl -X GET "https://api.ofself.ai/api/v1/exposure-profiles/default" \
-H "Authorization: Bearer your-jwt-token"
YAML Representation
Profiles can be viewed as YAML for human-readable inspection:
curl -X GET "https://api.ofself.ai/api/v1/exposure-profiles/profile_abc/yaml" \
-H "Authorization: Bearer your-jwt-token"
Common Profile Patterns
Read-Only Work Data
{
"name": "Work Read-Only",
"tag_permissions": {
"discover": { "allow_all": true },
"read": { "tag_ids": ["work-tag-id"], "allow_all": false },
"propose": { "allow_all": false },
"edit": { "allow_all": false },
"create": { "allow_all": false }
},
"tag_filters": {
"include_any": ["work"],
"exclude_any": ["confidential"]
}
}
Full Access (Trusted App)
{
"name": "Full Access",
"tag_permissions": {
"discover": { "allow_all": true },
"read": { "allow_all": true },
"propose": { "allow_all": true },
"edit": { "allow_all": true },
"create": { "allow_all": true }
}
}
AI Agent with Propose-Only
{
"name": "AI Agent - Read & Propose",
"tag_permissions": {
"discover": { "allow_all": true },
"read": { "allow_all": true },
"propose": { "allow_all": true, "allow_new": true },
"edit": { "allow_all": false },
"create": { "allow_all": false }
}
}
How Apps See Filtered Data
When your app calls the API with a user who has granted access via a profile:
# App calls list nodes
nodes = client.nodes.list(user_id="user-123")
# Only returns nodes matching the exposure profile:
# - Matching tag permissions (read access to relevant tags)
# - Matching tag filters (include/exclude rules)
# - Matching node type filters
# - Within date range (if configured)
The API automatically filters based on the active exposure profile.
Best Practices
1. Request Minimum Permissions
When registering your app, only request what you need:
{
"requested_permissions": {
"tags": { "discover": true, "read": true, "propose": true },
"schemas": { "read": { "schema_ids": [], "allow_all": true }, "write": { "schema_ids": [], "allow_all": false } }
}
}
2. Explain What You Need
When users authorize your app, they see your requested permissions. Make sure your app description clearly explains why each permission is needed.
3. Respect the Scope
Even if you have the API key, only access what the exposure profile allows. Attempting to access out-of-scope data will return 403 errors.
4. Support Multiple Profiles
Users can create different profiles for different use cases. Your app operates within whatever profile the user selects during authorization.
Permission Enforcement Model
The Paradigm SDK uses a three-layer permission model:
1. Requested Permissions (Developer)
When registering your app, you declare what permissions it needs:
{
"requested_permissions": {
"tags": {
"discover": true,
"read": true,
"propose": true,
"create": false
}
}
}
These permissions:
- Are displayed to users during authorization
- Act as a ceiling - your app cannot exceed them
- Should reflect what your app actually needs
2. Granted Permissions (User)
Users select an exposure profile that determines:
- Which tags your app can access
- Which actions are allowed on those tags
- Which schemas can be read or written
3. Effective Permissions (Runtime)
At runtime, your app gets the intersection of requested and granted:
┌─────────────────────────────────────────────────────────────────┐
│ Permission Flow │
│ │
│ App Requests User Grants Effective │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ read ✓ │ ∩ │ read ✓ │ = │ read ✓ │ │
│ │ create ✓ │ │ create ✓ │ │ create ✓ │ │
│ │ edit ✗ │ │ edit ✓ │ │ edit ✗ │ ← blocked│
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ Even though user granted "edit", app can't use it because │
│ it wasn't in the app's requested_permissions. │
└─────────────────────────────────────────────────────────────────┘
Why This Model?
- Transparency: Users see exactly what apps are asking for
- Security: Apps can't silently use more permissions than declared
- Flexibility: Users can still grant different profiles to different apps
- Trust: Users can verify that apps don't ask for unnecessary permissions
Authorization UI
During authorization, users see:
- "This App is Requesting" - Shows the app's declared permissions
- Permission validation - Error if profile doesn't cover all requests
- Success indicator - Confirmation when profile covers all permissions
Authorization Blocking
Important: Authorization is blocked if the selected profile doesn't grant all permissions the app requested. The "Approve" button is disabled until the user either:
- Selects a different profile that covers the requirements
- Customizes permissions to include what the app needs
This ensures apps always receive the permissions they need to function correctly.
Customizing Permissions During Authorization
Users can customize permissions when authorizing an app:
- Select an existing profile as a starting point
- Click "Customize Permissions" to modify tag/schema permissions
- When done, choose one of:
- Update existing profile: Overwrite the selected profile with custom permissions
- Save as new profile: Create a new profile with a unique name
This allows users to fine-tune access without leaving the authorization flow.
Deleting Profiles
Deletion Rules
| Scenario | Result |
|---|---|
| Profile is the default | Cannot delete (409 error) |
| Profile has active authorizations | Cannot delete - revoke authorizations first |
| Profile has only revoked authorizations | Can delete - revoked records are auto-deleted |
| Profile has no authorizations | Can delete |
Example: Deleting a Profile
# First, revoke any active authorizations using this profile
client.authorizations.revoke(authorization_id="auth-123")
# Then delete the profile
client.exposure_profiles.delete(
user_id="user-123",
profile_id="profile_work123"
)
When you delete a profile, any revoked authorization records that referenced it are automatically deleted. The audit log preserves the history of those authorizations.