.dev
Apps & OAuth

App data buckets

Namespaced JSON storage on apps and per-user pivot rows — public, private and protected tiers.

Every app gets JSON storage with visibility tiers — useful for publishing app metadata, keeping server-side configuration, and storing per-user integration state without running your own database.

The two storage locations

On the app itself (one per app):

BucketReadWrite
publicAnyone, no authApp owner
privateApp ownerApp owner

On the (app, user) pivot (one per Feeef user who interacts with your app):

BucketReadWrite
publicUser + developerUser + developer
privateDeveloper onlyDeveloper only
protectedUser + developerDeveloper only

protected is the interesting one: the user can see it but not forge it — right for entitlements, verification flags, subscription status.

JSON shape and limits

  • Root must be an object; every top-level key must map to an object (a "namespace"). Top-level arrays, primitives and null are rejected with 422.
  • Max 64 KiB serialized per bucket.
{
  "settings": { "theme": "dark" },
  "integrations": { "slack": { "enabled": true } }
}

App-level endpoints

CallAuthNotes
GET /apps/:id/public-dataNone404 if app missing or inactive
PUT /apps/:id/public-dataOwnerBody { "public": {...} } — full replacement
GET /apps/:id/private-dataOwnerReturns { "private": {...} }
PUT /apps/:id/private-dataOwnerFull replacement

The dashboard's Data tab edits both buckets with validation.

Per-user endpoints

User side — any signed-in Feeef user, for any active app:

CallReturns / accepts
GET /apps/:id/user-data/me{ "public": {...}, "protected": {...} }private is never returned
PUT /apps/:id/user-data/meOnly { "public": {...} } — including private/protected keys → 422

Developer side — requires a token issued via the authorization-code flow for this app, carrying the apps scope (or *), acting on the token's own user (:userId must equal the token user):

CallReturns / accepts
GET /apps/:id/user-data/users/:userIdAll three buckets
PUT /apps/:id/user-data/users/:userIdAny subset of public / private / protected — omitted buckets stay unchanged (partial update)

403 causes: token not bound to this app, missing apps scope, :userId mismatch, or a first-party login token (no app_id) on a developer route.

Typical pattern

Register your app with apps in its scopes, and request scope=auth apps at authorize.

After code exchange, the token row stores your app_id — the key that unlocks the developer user-data routes.

Your backend writes protected state (plan, entitlements) after its own checks; your client reads it back via GET /apps/:id/user-data/me and trusts it because users can't write it.

SDK methods

Both SDKs expose the same surface (JS feeef.apps.* / Dart Feeef.instance.apps.*):

HTTPSDK method
GET …/public-datagetPublicData(appId)
PUT …/public-dataputPublicData(appId, data)
GET …/private-datagetPrivateData(appId)
PUT …/private-dataputPrivateData(appId, data)
GET …/user-data/megetUserDataMe(appId)
PUT …/user-data/meputUserDataMe(appId, data)
GET …/user-data/users/:userIdgetUserDataForUser(appId, userId)
PUT …/user-data/users/:userIdputUserDataForUser(appId, userId, body)

GET …/public-data is unauthenticated by design — never put secrets in a public bucket, and expect it to be cached. protected should be written only from your backend.

On this page