📣New ways to setup permissions for shareable content. Read the release notes.

    Testing

    Simpler Auth, But Just as Powerful

    TLDR

    We make adding authentication to your Triplit app easier than ever.

    Auth in Triplit

    Triplit's rules and permissions system gives you a powerful and flexible way to control access to your data. With it you can:

    • use any third-party authentication provider that supports JWTs
    • express permissions in terms of query filters, which can be arbitrarily complex and relational
    • define several (potentially overlapping) roles based on the shape of the client's JWT, each with its own permissions

    Once you get it up and running, there are few limits on the complexity of the rules you can express. But getting it up and running has been a pain point for some users. So this week's theme is making it easier to get started with authentication and authorization in Triplit.

    New and updated guides

    We now have a quickstart guide that walks you through setting up authentication with an external provider and adding basic permissions to your Triplit App. It covers:

    • choosing a provider
    • making sure Triplit can verify the JWTs issued by your provider
    • configuring the Triplit client to use the JWT
    • defining a schema with permissions
    • writing data in a way that attributes the data to the token bearer
    • common issues and methods for debugging them

    We've also updated our Clerk and Supabase integration guides to reflect the new authentication flow.

    New default roles

    Previously, to use permissions in Triplit, you had to define at least one role. A role is a named key and a JWT claim matching pattern:

    const roles: Roles = {
      user: {
        match: {
          type: 'user',
          sub: '$userId',
        },
      },
    };
    

    We've now added two default roles that are automatically applied to any client that connects to your Triplit instance if you don't define any roles yourself:

    • anonymous: will be applied to clients connecting with the Anon token. This is useful for apps that are usable without unique user authentication (e.g. a public dashboard).
    • authenticated: will be applied to clients connection with a token that has a sub claim. The sub claim is a standard JWT claim that identifies the subject (user) of the token with a unique identifier. Most authentication providers will include this claim in the token they issue.

    You might use these default roles like this:

    import { Schema as S } from '@triplit/client';
    
    const schema = S.Collections({
      posts: {
        schema: S.Schema({
          id: S.Id(),
          title: S.String(),
          body: S.String(),
          authorId: S.String(),
        }),
        permissions: {
          anonymous: {
            read: { filter: [true] },
          },
          authenticated: {
            read: { filter: [true] },
            insert: { filter: [['authorId', '=', '$token.sub']] },
            update: { filter: [['authorId', '=', '$token.sub']] },
            delete: { filter: [['authorId', '=', '$token.sub']] },
          },
        },
      },
    });
    

    Which allows any client to read any post, but only the author of a post to update or delete it.

    Newly renamed $token variables

    When writing queries or defining permissions, you'll often need to refer to a client's JWT claims. This has always been possible with Triplit via the $session variable scope, but to better reflect that this scope strictly contains all of the claims on the token, we've aliased it to $token. You can refer to any of the standard claims like $token.sub and $token.iat or custom claims like $token.email if your authentication provider includes them. These are patterns you'll want to adopt when defining permissions without custom roles. With custom roles, you have the flexibility to alias claims to the on a JWT token to the $role variable scope. Read more about custom roles here.

    TriplitClient.vars for cleaner code

    You'll often want to include user-identifying information to an entity in Triplit, like the sender of a message or the author of a blog post, be it for defining permissions or adding a social element to your app. It's best if you use your authentication provider and their unique identifiers as the source of truth for this information (e.g. better to use the provider's userId's than define your own user collection in Triplit). Getting this information from your authentication provider can be tricky depending on the robustness of the client library they provide.

    Because of this, we've added a property to the TriplitClient (and WorkerClient) called vars. At the time of this release, the $token and $global variable scopes will be accessible in this object. It unlocks some very symmetric permission and mutation patterns. For example, if you have a schema for a diary app that only allows the author of a diary entry to read it, it might have permissions like these that reference $token.sub to establish the author of the entry:

    import { Schema as S } from '@triplit/client';
    
    const schema = S.Collections({
      diaryEntries: {
        schema: S.Schema({
          id: S.Id(),
          title: S.String(),
          body: S.String(),
          createdAt: S.Date(),
          authorId: S.String(),
        }),
        permissions: {
          authenticated: {
            read: { filter: [['authorId', '=', '$token.sub']] },
            insert: { filter: [['authorId', '=', '$token.sub']] },
            update: { filter: [['authorId', '=', '$token.sub']] },
            delete: { filter: [['authorId', '=', '$token.sub']] },
          },
        },
      },
    });
    

    And when writing your client mutators to insert a diary entry, you can use the $token variable scope to set the authorId field to the user's sub claim:

    import { TriplitClient } from '@triplit/client';
    
    const client = new TriplitClient({
      serverUrl: 'https://<project-id>.triplit.io',
    });
    
    function onCreateDiaryEntry() {
      const diaryEntry = {
        title: 'My first diary entry',
        body: 'I had a great day today!',
        createdAt: new Date(),
        authorId: client.vars.$token.sub,
      };
    
      await client.insert('diaryEntries', diaryEntry);
    }
    

    Under-the-hood query improvements

    We've made strides this week in removing duplicate filter conditions from queries, a state that would sometimes occur in relational queries with complex relational permissions. We've also made some additional improvements to the query engine to make filter evaluation more efficient. In both cases we've seen modest improvements to query performance.

    Experimenting with running SQLite in a separate thread

    The Triplit backend server is a NodeJS server that persists data to SQLite. This has the advantages of co-locating the server and persistence layer, but can also lead to performance bottlenecks when the server is under heavy load. To mitigate this, we're experimenting with running SQLite in a separate thread. This allows us to offload some of the work to a separate thread, which can improve performance and responsiveness. We're still in the early stages of this experiment, but initial results are promising. When stable, we plan to make this the default behavior for all Triplit servers.

    You can test out this configuration with the development server by running triplit dev --storage sqlite-worker.