Pick your app

The examples below will be updated with your app ID.

Authentication and Permissions

Permissions

To secure user data, you can use Instant’s Rule Language. Our rule language takes inspiration from Rails’ ActiveRecord, Google’s CEL, and JSON. Here’s an example ruleset below

// instant.perms.ts
import type { InstantRules } from "@instantdb/react";

const rules = {
  todos: {
    allow: {
      view: "auth.id != null",
      create: "isOwner",
      update: "isOwner",
      delete: "isOwner",
    },
    bind: ["isOwner", "auth.id != null && auth.id == data.creatorId"],
  },
} satisfies InstantRules;

export default rules;

You can manage permissions via configuration files or through the Instant dashboard.

Permissions as code

With Instant you can define your permissions in code. If you haven't already, use the CLI to generate an instant.perms.ts file:

npx instant-cli@latest init

The CLI will guide you through picking an Instant app and generate these files for you. Once you've made changes to instant.perms.ts, you can use the CLI to push those changes to production:

npx instant-cli@latest push perms

Permissions in the dashboard

For each app in your dashboard, you’ll see a permissions editor. Permissions are expressed as JSON. Each top level key represents one of your namespaces — for example goals, todos, and the like. There is also a special top-level key attrs for defining permissions on creating new types of namespaces and attributes.

Namespaces

For each namespace you can define allow rules for view, create, update, delete. Rules must be boolean expressions.

If a rule is not set then by default it evaluates to true. The following three rulesets are all equivalent

In this example we explicitly set each action for todos to true

"todos": {
  "allow": {
    "view": "true",
    "create": "true",
    "update": "true",
    "delete": "true"
  },

In this example we explicitly set view to be true. However, all the remaining actions for todo also default to true.

"todos": {
  "allow": {
    "view": "true"
  },

In this example we set no rules, and thus all permission checks pass.

{}

When you start developing you probably won't worry about permissions. However, once you start shipping your app to users you will want to secure their data!

View

view rules are evaluated when doing db.useQuery. On the backend every object that satisfies a query will run through the view rule before being passed back to the client. This means as a developer you can ensure that no matter what query a user executes, they’ll only see data that they are allowed to see.

Create, Update, Delete

Similarly, for each object in a transaction, we make sure to evaluate the respective create, update, and delete rule. Transactions will fail if a user does not have adequate permission.

Default permissions

By default, all permissions are considered to be "true". To change that, use "$default" key. This:

"todos": {
  "allow": {
    "$default": "false"
  }
}

is equivalent to this:

"todos": {
  "allow": {
    "view": "false",
    "create": "false",
    "update": "false",
    "delete": "false",
  }
}

Specific keys can override defaults:

"todos": {
  "allow": {
    "$default": "false",
    "view": "true"
  }
}

You can use $default as the namespace:

"$default": {
  "allow": {
    "view": false
  }
},
"todos": {
  "allow": {
    "view": "true"
  }
}

Finally, the ultimate default:

"$default": {
  "allow": {
    "$default": false
  }
}

Attrs

Attrs are a special kind of namespace for creating new types of data on the fly. Currently we only support creating attrs. During development you likely don't need to lock this rule down, but once you ship you will likely want to set this permission to false

Suppose our data model looks like this

{
  "goals": { "id": UUID, "title": string }
}

And we have a rules defined as

{
  "attrs": { "allow": { "create": "false" } }
}

Then we could create goals with existing attr types:

db.transact(db.tx.goals[id()].update({title: "Hello World"})

But we would not be able to create goals with new attr types:

db.transact(db.tx.goals[id()].update({title: "Hello World", priority: "high"})

CEL expressions

Inside each rule, you can write CEL code that evaluates to either true or false.

{
  "todos": {
    "allow": {
      "view": "auth.id != null",
      "create": "auth.id in data.ref('creator.id')",
      "update": "!(newData.title == data.title)",
      "delete": "'joe@instantdb.com' in data.ref('users.email')"
    }
  }
}

The above example shows a taste of the kind of rules you can write :)

data

data refers to the object you have saved. This will be populated when used for view, create, update, and delete rules

newData

In update, you'll also have access to newData. This refers to the changes that are being made to the object.

bind

bind allows you to alias logic. The following are equivalent

{
  "todos": {
    "allow": {
      "create": "isOwner"
    },
    "bind": ["isOwner", "auth.id != null && auth.id == data.creatorId"]
  }
}
{
  "todos": {
    "allow": {
      "create": "auth.id != null && auth.id == data.creatorId"
    }
  }
}

bind is useful for not repeating yourself and tidying up rules

{
  "todos": {
    "allow": {
      "create": "isOwner || isAdmin"
    },
    "bind": [
      "isOwner",
      "auth.id != null && auth.id == data.creatorId",
      "isAdmin",
      "auth.email in ['joe@instantdb.com', 'stopa@instantdb.com']"
    ]
  }
}

ref

You can also refer to relations in your permission checks. This rule restricts delete to only succeed on todos associated with a specific user email.

{
  "todos": {
    "allow": {
      "delete": "'joe@instantdb.com' in data.ref('users.email')"
    }
  }
}

ref works on the auth object too. Here's how you could restrict deletes to users with the 'admin' role:

{
  todos: {
    allow: {
      delete: "'admin' in auth.ref('$user.role.type')",
    },
  },
};

See managing users to learn more about that.

Previous
Clerk