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 theAnon
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 asub
claim. Thesub
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
.