# Triplit Documentation > Triplit is an open-source database that syncs data between server and browser in real-time. This documentation covers all aspects of using Triplit, from installation to advanced usage. # Core Documentation ## faq # Frequently asked questions ## Why should I choose Triplit over a traditional database? Triplit is designed to have the best developer experience possible right out of the box. It was created to make building apps with real-time, local-first user experiences much easier than with existing tools. So if you aspire to have your app to feel native while supporting collaborative features expected of modern web apps, then Triplit is probably the right choice for you. ## Why should I care about offline support? By adding offline support to your app, you end up making it fast in _all_ network conditions. Even if your users are on fast internet and you've fully optimized your server, you can't beat the speed of light. Having a robust cache on device will just improve your user experience by making each interaction feel instant and not delayed by a roundtrip to the server. This is same strategy employed by some of the most loved apps like Linear, Figma, and Superhuman. ## How can Triplit be a relational database if it doesn't use joins? In Triplit, relationships are simply sub-queries. They allow you to connect entities across collections (or even within the same collection) with the expressiveness of a query. Triplit's support for set attributes allows it to establish complex relations without join tables. Sets can be used to "embed" the related ids directly on the entity. For example, a schema for a chat app with users and messages could be defined as follows: ```typescript const schema = S.Collections({ users: { schema: S.Schema({ id: S.String(), name: S.String(), }), }, messages: { schema: S.Schema({ id: S.Id(), text: S.String(), likes: S.Set(S.String()), }), relationships: { users_who_liked: S.RelationMany({ collectionName: 'users', where: [['id', 'in', '$likes']], }), }, }, }); ``` Check out [relations in your schema](/schemas/relations) to learn more. ## How does Triplit handle multiple writers / collaboration / multiplayer? Every entity inserted into Triplit is broken down at the attribute level, and each attribute is assigned a unique timestamp. This means when multiple users change different attributes of the same entity, they don't conflict or collide. When two users do update the same attribute at the same time, we use these timestamps to decide which value will be kept. In the literature this type of data structure is called a CRDT or more specifically a Last Writer Wins Register that uses Lamport timestamps. A [CRDT](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type) is a Conflict-Free Replicated Data Type. It's a name for a family of data structures that can handle updates from multiple independent writers and converge to a consistent, usable state. ## Does Triplit support partial replication? Yes. We believe that partial replication is the only practical way to make applications fast over the network. When Triplit clients subscribe to specific queries the Triplit servers only send data to the clients that are listening for it and that have not yet received it. Triplit's sync protocol sends only 'deltas' between the client and server to minimize latency and network traffic. ## How do I connect to Triplit from a server? You can use the [HTTP Client](/client/http-client) to query and update data from a server, serverless function, command line interface, or anywhere that supports HTTP. You can also interface with the [HTTP REST endpoints](/http-api) directly. ## Why does Triplit support sets but not arrays? Every data structure that Triplit can store is designed to support multiple concurrent writers. Sets are able to handle concurrent additions and removals without problem, but arrays lose many nice properties under collaboration. Consider the push method: if two people concurrently push to an array they will end up adding an element to the same index, ultimately causing one item to overwrite the other. In the future, Triplit will expose a List datatype that will support pushing to the beginning and end of a list and assignment operations to specific indices. In most cases using a Set or a List in place of an Array will suffice. ## Why do ordinary databases struggle with collaborative applications? Many popular databases do not implement real-time query subscriptions, which are the foundation for collaborative apps. Developers generally end up replicating the functionality of their remote database on the client to create the illusion of live updating queries. --- ## http-api # HTTP API ## Overview The HTTP API is a RESTful API that allows you to interact with a Triplit Cloud production server or the Triplit [Node server](https://github.com/aspen-cloud/triplit/tree/main/packages/server) that you can host yourself. It's useful if your client can't connect over WebSockets, or if your application wants to forgo the local cache and optimistic updates that the Triplit sync protocol provides. This can be useful for applications that need certainty about the state of the database, or for migrating data to Triplit from other services. ## Authentication The HTTP API, like the Triplit sync protocol, uses [JSON Web Tokens](https://jwt.io/) (JWT) for authentication. If you're communicating with a Triplit Cloud production server, you'll need to use your project's Service or Anonymous Token from the [Triplit Cloud dashboard](https://triplit.dev/dashboard) for your project. If you're communicating with a Node server that you control, you'll need a properly formed JWT with the correct claims. Using the [Triplit CLI](/local-development) and `triplit dev` command will automatically generate acceptable Service and Anonymous tokens for you. With your token in hand, set up your HTTP client to send the token in the `Authorization` header with the `Bearer` scheme. Using the `Fetch` API, it would look like this: ```ts // Request await fetch('https://<project-id>.triplit.io/<route>', { headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + TRIPLIT_TOKEN, }, }); ``` ## `TriplitClient.http` and `HttpClient` Triplit provides helpful abstractions for interacting with the HTTP API. Read more about it in the [Triplit Client documentation](/client/http-client). ## Routes ### `/fetch` Performs a fetch, returning the an array of entities that meet the query criteria. ```ts // Request await fetch('https://<project-id>.triplit.io/fetch', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + TRIPLIT_TOKEN, }, body: JSON.stringify({ query: { collectionName: 'todos', where: [['completed', '=', false]], }, }), }); // Response [ { id: '123', title: 'Buy milk', completed: false, }, { id: '456', title: 'Buy eggs', completed: false, }, ]; ``` ### `/insert` Inserts a single entity for a given collection. ```ts // Request await fetch('https://<project-id>.triplit.io/insert', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + TRIPLIT_TOKEN, }, body: JSON.stringify({ collectionName: 'todos', entity: { id: '123', title: 'Buy milk', completed: false, }, }), }); ``` ### `/bulk-insert` Inserts several entities at once that are provided as an object where the collection names are the keys and the list of entities for that collection are the values. ```ts // Request await fetch('https://<project-id>.triplit.io/bulk-insert', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + TRIPLIT_TOKEN, }, body: JSON.stringify({ todos: [ { id: '123', title: 'Buy milk', completed: false, }, { id: '456', title: 'Buy eggs', completed: false, }, ], }), }); ``` ### `/update` Updates a single entity for a given collection with a set of provided patches. ```ts // Request await fetch('https://<project-id>.triplit.io/update', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + TRIPLIT_TOKEN, }, body: JSON.stringify({ collectionName: 'todos', entityId: '123', patches: [ ['set', 'completed', true], ['set', 'title', 'Buy milk and eggs'], ], }), }); ``` ### `/delete` Deletes a single entity for a given collection. ```ts // Request await fetch('https://<project-id>.triplit.io/delete', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + TRIPLIT_TOKEN, }, body: JSON.stringify({ collectionName: 'todos', entityId: '123', }), }); ``` ### `/delete-all` Deletes all entities for a given collection. ```ts // Request await fetch('https://<project-id>.triplit.io/delete-all', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + TRIPLIT_TOKEN, }, body: JSON.stringify({ collectionName: 'todos', }), }); ``` ### `/healthcheck` This endpoint is publicly available (i.e. no authentication token is required) and will return a 200 status code if the server is running and healthy. --- ## local-development # Local Development Although you can connect directly to a managed or self hosted instance of a remote Triplit Database, you can also run a local instance of Triplit Database for development purposes. This not only helps separate your development and production environments, but Triplit's local development toolkit provides a lot of help for deploying updates to the Database. For this reason, we recommend running a local instance of Triplit Database when building most projects. ## Install the CLI If you haven't already, install the Triplit CLI and initialize a project. You can find instructions for doing so [here](/getting-started). ## Start Triplit services To start Triplit services, run the following command in your project directory: ```bash npx triplit dev ``` By default this will start the following services: - Triplit Console, running at `https://console.triplit.dev/local` - Triplit Database server, running at `http://localhost:6543` And prints a usable Service Token and Anonymous Token for connecting to the database. ## Additional environment variables If your project has a `.env` file, you may set the following environment variables to configure the Triplit Database server: If you're using a framework like [Vite](https://vitejs.dev/guide/) or [Next.js](https://nextjs.org/docs) you should add additional environmental variables prepended with `VITE_` or `NEXT_PUBLIC_` respectively for the `DB_URL` and `ANONYMOUS_TOKEN`. For example, `TRIPLIT_DB_URL` would become `VITE_TRIPLIT_DB_URL` or `NEXT_PUBLIC_TRIPLIT_DB_URL`. - `TRIPLIT_SERVICE_TOKEN` - The Service Token to use for connecting to the database for CLI commands. If not set, you may use a flag in the CLI (which takes precedent) or the CLI will prompt you for a key. - `TRIPLIT_DB_URL` - The URL to use for connecting to the database. If not set, you may use a flag in the CLI (which takes precedent) or use the default URL for the local database. - `TRIPLIT_JWT_SECRET` - Overrides the JWT secret used when generating local api tokens. - `TRIPLIT_EXTERNAL_JWT_SECRET` - Overrides the JWT secret used when generating external api tokens. - `TRIPLIT_CLAIMS_PATH` - A `.` separated path to read Triplit claims on external api tokens. If not set, claims are assumed to be at the root of the token. - `TRIPLIT_MAX_BODY_SIZE` - The maximum body size for incoming requests. This is useful if you want to send large payloads to your server. The default value is 100, corresponding to 100MB. --- ## offline-mode # Offline mode Apps that use Triplit work offline by default. No extra configuration is needed. All Triplit clients save data to a local cache and mutate that cache optimistically without waiting for the server to respond. Mutations that are made offline queue in an "outbox" and are sent to the server when the client comes back online. Triplit's CRDT-based sync protocol ensures that data is eventually consistent across all clients, even those that have been offline for an extended period. ## Storage By default, Triplit clients use memory storage for the local cache. While offline, an app using memory storage will make optimistic reads and writes, and be fully functional. However, when the page is refreshed is closed or refreshed, data saved in memory storage is lost, as are any pending changes in the client's outbox. To persist data across sessions, including the outbox, you can use the [`IndexedDB` storage adapter](/client/storage#indexeddb). ```ts const client = new TriplitClient({ storage: 'indexeddb', serverUrl: VITE_TRIPLIT_SERVER, token: VITE_TRIPLIT_TOKEN, }); ``` Now not only will a user's cache be persisted across app loads, leading to faster startup times, but if users make offline changes and then close the tab, their changes will be saved and sent to the server whenever they next use the app in a connected state. ## Syncing ### Partial replication Triplit clients uses a partial replication strategy to sync data with the server. This leads to faster load times, a small storage footprint in the browser, and less data transferred over the network. Triplit clients will request the latest data from the server only for the those queries that they are [subscribed to](/client/subscribe) and have [permission to view](/schemas/permissions). When the server has new data to share, it will send only the attribute-level changes that the client has not yet seen. ### Connected states Triplit clients expose their current connection status with `connectionStatus` and `onConnectionStatusChange` properties, or in a given framework using the appropriate hook (e.g. [`useConnectionStatus`](/frameworks/react#useconnectionstatus) in `@triplit/react`). These can be used to render UI elements that indicate whether the client is online or offline. The Triplit client also exposes methods `connect()` and `disconnect()` that can be used to manually control the client's connection status. ## Local-only mode If you want to use Triplit without syncing data to a server, you can set the [`autoConnect`](/client/options#sync-options) option to `false` or omit the `serverUrl` and `token` options when creating a client. In this mode, the client will still save data to a local cache and make optimistic updates, and no data will be sent to the server. If you are using a form of durable storage, such as `indexeddb`, data will persist across app loads, and can eventually be synced to a server in a later iteration of your app. --- ## quick-start import { Callout, Tabs, Tab, Steps, Collapse, FileTree, } from 'nextra-theme-docs'; # Quick Start ### Install Triplit to your app #### New projects The fast way to get started with Triplit is to use Create Triplit App which will scaffold a full stack application with Triplit. #### Existing projects If you have an existing project, you can install the `triplit init` command to create the necessary files and folders and install the required dependencies. <Tabs items=> <Tab> ```bash copy npm install --save-dev @triplit/cli ``` </Tab> <Tab> ```bash copy pnpm add --save-dev @triplit/cli ``` </Tab> <Tab> ```bash copy yarn add --dev @triplit/cli ``` </Tab> <Tab> ```bash copy bun add --dev @triplit/cli ``` </Tab> </Tabs> Follow the instructions in the [React + Vite tutorial](/react-tutorial) to finish your setup. The tutorial uses React but is applicable to any framework. ### Create your Schema <FileTree> <FileTree.Folder name="triplit" defaultOpen> </FileTree.Folder> </FileTree> You'll find a schema already setup in your project. You can modify this file to define the collections in your database. To learn more about schemas, check out the [schema guide](/schemas). ### Configure your client <FileTree> <FileTree.Folder name="triplit" defaultOpen> </FileTree.Folder> </FileTree> To sync with a Triplit server, your frontend needs a `TriplitClient`. Create a `client.ts` file in your `triplit` folder and export a client. ```typescript filename="triplit.ts" copy export const triplit = new TriplitClient({ schema, serverUrl: 'http://localhost:6543', token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ4LXRyaXBsaXQtdG9rZW4tdHlwZSI6InNlY3JldCIsIngtdHJpcGxpdC1wcm9qZWN0LWlkIjoibG9jYWwtcHJvamVjdC1pZCJ9.8Z76XXPc9esdlZb2b7NDC7IVajNXKc4eVcPsO7Ve0ug', }); ``` This snippet is configured to connect with a default local development server. Once you've deployed your own server or are using Triplit Cloud, you'll need to update the `serverUrl` and `token` fields and store them as environmental variables. ### Setup your dev server Triplit has a robust local development environment that you can set up with a single command. ```bash copy npx triplit dev ``` The `dev` command will start a local Triplit server with your schema applied. To learn more about the dev server, check out the [local development guide](/local-development). ### Insert and Query Data To add data to your database you can use the pre-configured client to [`insert`](/client/insert) data. ```typescript await triplit.insert('todos', ); ``` To run a query and subscribe to changes to that query, you can use [`subscribe`](/client/subscribe). ```typescript const query = triplit.query('todos').Where('completed', '=', false); const unsubscribe = triplit.subscribe(query, (data) => { console.log(data); }); ``` There are also specific bindings a number of popular frameworks like [React](/frameworks/react) and [Svelte](/frameworks/svelte) that you can use to interact with your data. See the `Frameworks` section for a full list of supported integrations and details on usage. ### Deploy When you're ready to go live, you'll need Triplit Server running. Check out the other guides to learn how you can [self host your own instance](/triplit-cloud/self-hosted-deployments) or [deploy on Triplit Cloud](/triplit-cloud/managed-machines). With your server running and your `.env` setup, you can now push your schema to the server ```bash copy npx triplit schema push ``` That's it! But there's a lot more you can do with Triplit. Check out the rest of the docs to learn more about how to define [relations in your schema](/schemas/relations), [write transactions](/client/transact), add [access control](/schemas/rules), and more. --- ## react-tutorial If you're not interested in building the app from scratch, you can get a fully working demo app by running `npm create triplit-app@latest`. # Build a Todos app with React, Vite and Triplit This tutorial will show you how to build a simple Todos app with React, Vite and Triplit. It will cover the following topics: - How to create a new Triplit project - How to create a new React app with Vite - How to create a Triplit schema for the Todos app - How to use the Triplit console - How to read and mutate data with Triplit - How to sync data with Triplit The app will be built with: - [React](https://reactjs.org/) as the UI framework - [Vite](https://vitejs.dev/) as the build tool - [Triplit](/) as the database and sync engine If at any time you want to check your progress against the finished app, or if you get stuck, you can find the source code for the finished app [here](https://github.com/aspen-cloud/triplit-react-todos). ## Project setup ### Create a new project Let's use Vite to create a new React app. Run the following command in your terminal: ```bash copy npm create vite@latest todos -- --template react-ts ``` Follow the prompts to create a new project. Once it's been created, navigate to the project directory: ```bash copy cd todos ``` ### Add in Triplit Before we start building the app, we need to integrate Triplit and its dependencies project. You can do this by installing the [Triplit CLI](/cli), the [React bindings](/frameworks/react), and running the `init` command. ```bash copy npm install -D @triplit/cli npx triplit init --framework react ``` This will will create some Triplit-specific files and add the necessary dependencies. The directory structure for your new project should look like this: ``` /triplit schema.ts /public [static files] /src [app files] [other files] ``` ### Define the database schema Triplit uses a [schema](/schemas) to define the structure of your data. By using a schema, Triplit can validate your data at runtime and provide autocompletion in your editor. We're going to set up a [schema](/schemas) for the Todos app in the `./triplit/schema.ts` file. Open the file and replace its contents with the following code: ```ts filename="./triplit/schema.ts" copy export const schema = S.Collections({ todos: { schema: S.Schema({ id: S.Id(), text: S.String(), completed: S.Boolean(), created_at: S.Date(), }), }, }); ``` This schema defines a single collection, `todos`, with the following fields: - `id`: A unique identifier for the todo. Every Triplit collection must have an `id` field. Defaults to a random string if not specified. - `text`: A string that contains the todo text. - `completed`: A boolean value that indicates whether the todo is completed or not. Defaults to `false` if not specified. - `created_at`: A timestamp that indicates when the todo was created. Defaults to the current time if not specified. ### Start the development server Triplit provides a development server that you can use to test your app locally. To start the development server, run the following command: ```bash copy npx triplit dev ``` This will start the sync server at `http://localhost:6543` and a database console at `https://console.triplit.dev/local`. It will also output API tokens that your app will use to connect to the sync server. If you're using our hosted service, Triplit Cloud, you can use the API tokens and url shown on the [dashboard](https://triplit.dev/dashboard) for your project page. The instructions in the rest of this tutorial are the same whether you're using Triplit Cloud or running the sync server locally. Now's a good time to set up an `.env` file in the `todos` directory. This file will contain the API tokens that your app will use to connect to the sync server. Create a new file called `.env` in the `todos` directory and add the following: ```bash filename=".env" copy TRIPLIT_DB_URL=http://localhost:6543 TRIPLIT_SERVICE_TOKEN=replace_me TRIPLIT_ANON_TOKEN=replace_me # Replace `replace_me` with the tokens in the terminal where you ran `npx triplit dev` VITE_TRIPLIT_SERVER_URL=$TRIPLIT_DB_URL VITE_TRIPLIT_TOKEN=$TRIPLIT_ANON_TOKEN ``` Make sure you have `.env` as part of your `.gitignore` file: ```bash filename=".gitignore" copy # Ignore .env files .env ``` Now restart the development server by pressing `Ctrl + C` and running `npx triplit dev` again. We're basically done setting up Triplit. Now we can start building the app. The development server automatically loads your schema on startup. If you're using Triplit Cloud, run `npx triplit schema push` to make the remote server aware of the schema you've defined. ## Building the app Let's start building the app. ### Getting started with Vite Vite is a build tool that makes building apps fast and easy. We already installed Vite above, so let's start using it. In a new terminal, in the `todos` directory, run the following command to start the Vite development server: ```bash copy npm run dev ``` This will start the Vite development server on port `5173`. You can now open your browser and navigate to `http://localhost:5173` to see the app. ### The Triplit client Now that we have the development server running, let's integrate Triplit into our client code. Triplit provides a client library that you can use to read and write data. Let's initialize it with our API tokens and the schema that we defined earlier. Create a new file in the `triplit` directory called `client.ts` and add the following code: ```ts filename="triplit/client.ts" copy export const triplit = new TriplitClient({ schema, serverUrl: import.meta.env.VITE_TRIPLIT_SERVER_URL, token: import.meta.env.VITE_TRIPLIT_TOKEN, }); ``` Any time you want to read or write data, you'll import the `triplit` object from this file. ### Optional: styling The Vite app template comes with some basic styles. Feel free to replace them with your own styles or some of ours. Replace the contents of index.css with the following: ```css filename="src/index.css" copy :root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; } .app { display: flex; flex-direction: column; justify-content: center; width: 100%; max-width: 400px; gap: 16px; } .todo { display: flex; gap: 1em; align-items: center; margin-bottom: 8px; } .btn { margin-left: 8px; border: none; border-radius: 0.25em; padding: 0.5em 1em; background-color: cornflowerblue; color: #fff; font-size: 1em; cursor: pointer; } .todo-input { border-radius: 0.25em; border-color: cornflowerblue; padding: 0.5em 1em; background-color: #242424; color: #fff; font-size: 1em; } .x-button { border: none; background-color: transparent; display: none; } .x-button:hover { filter: brightness(1.5); } .todo:hover .x-button { display: block; } ``` ### Creating todos The Vite app template comes with an `App.tsx` file that contains some components. Let's replace it with an `` component that creates a new todo. Replace the contents of `App.tsx` with the following: ```tsx filename="src/App.tsx" copy showLineNumbers export default function App() { const [text, setText] = useState(''); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); await triplit.insert('todos', ); setText(''); }; return ( <div className="app"> <form onSubmit=> <input type="text" placeholder="What needs to be done?" className="todo-input" value= onChange= /> <button className="btn" type="submit" disabled=> Add Todo </button> </div> ); } ``` This component renders a form with a text input and a submit button. When the form is submitted, it calls `triplit.insert` to create a new todo. It uses React to update the text input's value and clear it when the form is submitted. Notice that in the call to `triplit.insert`, we're omitting several of the fields we defined in our schema. That's because those missing fields, `id`, `completed`, and `created_at`, have default values. Triplit will automatically fill in those fields with their default values. ### The Triplit console We have a component that creates a new todo, but we still need to write some code that fetches the todos from the database and renders them on the page. If we want to test that our insertions are working without doing any more work, we can use the Triplit console. When you ran `npx triplit dev` earlier, it started a Triplit console. This is located at `https://console.triplit.dev/local`. You should see a page that looks like this:  The console allows you to view and edit the contents of your database. You can use it to view the todos that you've created so far. You can also use it to create new todos or update existing ones. Add some todos from your Vite app, and watch them appear in the `todos` collection in the console. Then, in the console, click on a cell in the new rows that appear and update it. The Triplit console is super powerful. Not only can you mutate data, but you can apply filters, sorts, navigate relations and more. ### Creating a todo component Now that we have a way to create a new todo, let's create a component that renders a todo. Create a new `components` directory inside the `src` directory and a file called `Todo.tsx` and add the following code: ```tsx filename="src/components/Todo.tsx" copy type Todo = Entity<typeof schema, 'todos'>; export default function Todo(: ) { return ( <input type="checkbox" checked= onChange={() => triplit.update('todos', todo.id, async (entity) => { entity.completed = !todo.completed; }) } /> <button className="x-button" onClick={() => { triplit.delete('todos', todo.id); }} > ❌ </button> ); } ``` In this component, we're rendering a checkbox and some text describing our todo. The `Todo` component takes a single prop, a `Todo` entity, and renders it. To get the `Todo` type from our schema we use the `Entity` generic type, and pass in our schema and the name of the collection (`'todos'`) that we want a type for. When the checkbox is clicked, we call `triplit.update` to update the todo's `completed` field. `triplit.update` takes three arguments: the name of the collection, the id of the entity to update, and a callback that updates the entity. When the ❌ button is clicked, we call `triplit.delete` to delete the todo. `triplit.delete` takes two arguments: the name of the collection and the id of the entity to delete. ### Rendering the todos Now that we have a component that renders a todo, let's render a list of todos. First, let's query the todos from the database. We're going to use the `useQuery` hook provided by Triplit to query the todos and store them as React state. At the top of `App.tsx`, add the following code: ```tsx filename="src/App.tsx" copy showLineNumbers function useTodos() { const todosQuery = triplit.query('todos').Order('created_at', 'DESC'); const = useQuery(triplit, todosQuery); return ; } export default function App() { const [text, setText] = useState(''); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); await triplit.insert('todos', ); setText(''); }; return ( <form onSubmit=> <input type="text" placeholder="What needs to be done?" className="todo-input" value= onChange= /> <button className="btn" type="submit" disabled=> Add Todo </button> </form> ); } ``` The `useQuery` hook takes two arguments: a Triplit client and a query. The query is created by calling `triplit.query` and passing in the name of the collection that we want to query. `useQuery` returns an object with the following properties: - `results`: An `Array<Todo>` that contains the results of the query. - `error`: An error object if the query failed, or `undefined` if the query succeeded. - `fetching`: A boolean that indicates whether the query is currently fetching data. One thing to notice is that we've added an `order` clause to the query. This will order the todos by their `created_at` field in descending order. This means that the most recently created todos will be at the top of the list. Triplit's query API supports a wide range of clauses, including `where`, `limit`, `offset`, `order`, and more. You can learn more about the query API [here](/query). Now we're going to render the todos in the `App` component. Add the following lines to the `App` component: ```tsx filename="src/App.tsx" copy showLineNumbers function useTodos() { const todosQuery = triplit.query('todos').Order('created_at', 'DESC'); const = useQuery(triplit, todosQuery); return ; } export default function App() { const [text, setText] = useState(''); const = useTodos(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); await triplit.insert('todos', ); setText(''); }; return ( <form onSubmit=> <input type="text" placeholder="What needs to be done?" className="todo-input" value= onChange= /> <button className="btn" type="submit" disabled=> Add Todo </button> </form> <div> </div> ); } ``` Here we've: 1. Imported our `` component 2. Called the `useTodos` hook to query the todos 3. Rendered the todos by iterating over the array with `Array.map` in our `` component. Triplit queries are *live*, meaning that you never need to manually refetch data. As other clients insert, update or delete data, your query will automatically update to reflect those changes. Even if you go offline, the query will listen for the changes that you make locally and update the query with those local changes. When you go back online, the Triplit will sync those local changes with the server and pull in any changes it missed while we were offline. ## Persisting data So far, Triplit has been storing data in-memory (the default for `TriplitClient`). That means that if you refresh the page and go offline, you'll lose your data. Triplit supports a variety of [storage options](/client/storage) that you can use to persist your data even between refreshes or if you go offline. [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) is a low level durable storage API built into all modern browsers. Update the `client.ts` file to use the `indexeddb` storage option: ```ts filename="triplit/client.ts" copy export const triplit = new TriplitClient({ storage: 'indexeddb', schema, serverUrl: import.meta.env.VITE_TRIPLIT_SERVER_URL, token: import.meta.env.VITE_TRIPLIT_TOKEN, }); ``` When you use the Triplit client to insert or update data, that data will persist to IndexedDB. Test it out: if you create a few todos and refresh your browser, you should see that your todos are still there. ## Testing out the sync Now that we have a working app, let's test out the sync. In one browser window, navigate to `http://localhost:5173`. Then, open a private browsing window and navigate to `http://localhost:5173`. You should see the same app in both tabs. Now, create a new todo in one tab. You should see the todo appear in the other tab. Try checking and unchecking the todo. You should see the changes reflected in the other tab. Triplit works offline as well - try disconnecting from the internet and creating a new todo in one of the windows. You should see the todo appear in the other tab when you reconnect. This is the power of syncing with Triplit! ## Next steps We've built a simple Todos app with React, Vite and Triplit. We've learned how to: - Create a new Triplit project - Create a new React app with Vite - Create a Triplit schema for the Todos app - Read and mutate data with Triplit - Sync data with Triplit And there are still a lot of things that we haven't covered. - The rest of Triplit's [query API](/query) to select, filter and paginate data - Triplit's [access control rules](/schemas/rules) to control who can read and write data - Triplit's [transaction API](/client/transact) - Triplit's [relational API](/schemas/relations) to establish relationships between collections and then [select data across those relationships](/query/select#selecting-related-entities) - The various [storage options](/client/storage) for your Triplit client's cache If you have any questions, feel free to reach out to us on [Discord](https://discord.gg/q89sGWHqQ5) or [Twitter](https://twitter.com/triplit_dev). --- ## seeding # Seeding a Triplit Database In this guide, we'll walk through how to use `triplit seed` to seed a database. ## Creating a seed file First, we'll need to create a seed file. This is a file that contains the data we want to seed the database with. We'll use the `triplit seed` command to this file. ```bash triplit seed create my-first-seed ``` This will create a file called `my-first-seed.ts` in the `triplit/seeds` directory. It will introspect your [schema](/schemas) defined in `./triplit/schema.ts`. It will use the schema to provide some initial type hinting to help ensure that the data in your seed adheres to the schema. For example, a schema file might look like this: ```ts filename="./triplit/schema.ts" copy const schema = S.Collections({ todos: { schema: S.Schema({ id: S.Id(), text: S.String(), completed: S.Boolean(), created_at: S.Date(), }), }, profiles: { schema: S.Schema({ id: S.Id(), name: S.String(), created_at: S.Date(), }), }, }); ``` And would result in a seed file that looks like this: ```ts filename="triplit/seeds/my-first-seed.ts" copy export default function seed(): BulkInsert<typeof schema> { return { todos: [], profiles: [], }; } ``` ## Editing the seed file Now that we have scaffolded a seed file, we can start adding data to it. Let's add a few todos: ```ts filename="triplit/seeds/my-first-seed.ts" copy export default function seed(): BulkInsert<typeof schema> { return { todos: [ { text: 'Buy milk', }, { text: 'Buy eggs', }, { text: 'Buy bread', }, ], profiles: [ { name: 'Cole Cooper', }, ], }; } ``` You can add whatever scripts you want to this file, including external libraries, as long as it exports a default function that adheres to the `BulkInsert` type (a record with collection names as keys and arrays of entities as values). ## Using the seed file Now that we have a seed file, we can use the `triplit seed run` command to seed the database. First, make sure that your environment variables are set up correctly, with `TRIPLIT_DB_URL` pointing to your database (be it a local dev server or a Triplit Cloud instance) and `TRIPLIT_SERVICE_TOKEN` set to a valid service token. Then, run the `triplit seed create` command with the name of the seed file as an argument: ```bash copy triplit seed run my-first-seed ``` You should see some output ```bash > Running seed file: my-first-seed.ts > Successfully seeded with my-seed.ts > Inserted 3 document(s) into todos > Inserted 1 document(s) into users ``` ## `triplit seed` variants ### `create` You can use the `--create` option to create a seed file with some helpful typescript. This will introspect your schema and provide some initial type hinting to help ensure that the data in your seed adheres to the schema. ```bash copy triplit seed create my-first-seed ``` ### `--all` You can define multiple seed files in the `triplit/seeds` directory. You can run them all at once by using the `--all` option. ```bash copy triplit seed run --all ``` ### `--file` You can also run a specific seed file, not necessarily located in `triplit/seeds/[seed-file].ts` by using the `--file` option. ```bash copy triplit seed run path/to/my-seed.ts ``` --- ## self-hosting # Self-hosting Triplit To enable sync, you need to run a Triplit server. The server is a Node.js application that talks to various Triplit clients over WebSockets and HTTP. You have several options for running the server: - A local [development server](/local-development) - Use [Docker](#docker) and a cloud provider that supports container deployments - Use a [one-click deploy](#one-click-deploy) to a cloud provider that is integrated with Triplit. - Build [a custom server](#building-a-custom-server) and use a cloud provider that supports Git-based deploys The guide on this page is specifically for standalone, self-hosted deployments that are not compatible with the Triplit Cloud Dashboard. We recommend that you instead follow [this guide](/triplit-cloud/self-hosted-deployments) for self-hosting your Triplit Server while still making it accessible and configurable via the Triplit Dashboard. ## Docker Each release of the server is [published as a Docker image](https://hub.docker.com/r/aspencloud/triplit-server/tags). You can deploy the server to a cloud provider like [fly.io](https://fly.io/docs/languages-and-frameworks/dockerfile/), [DigitalOcean](https://docs.digitalocean.com/products/app-platform/how-to/deploy-from-container-images/), or AWS. You'll also want to setup a volume to persist the database. The docker file starts a node server on port 8080, and you can pass in the following environment variables to configure the server: - `NODE_OPTIONS` - Node.js options for the server (e.g. `--max-old-space-size=4096`) ## One-click deploy Triplit is integrated with [Railway](https://railway.app/), a cloud provider that supports one-click deploys. Read about how to deploy a Triplit server using Railway in our [Triplit Cloud guide](/triplit-cloud/self-hosted-deployments). We plan on adding support for more cloud providers in the future. If you have a favorite cloud provider that you'd like to see integrated with Triplit, let us know by [joining our Discord](https://discord.gg/q89sGWHqQ5). ## Building a custom server The server is published as an NPM package, and you can install it by running: ```bash copy npm install @triplit/server ``` The server also contains the remote Triplit database, which will persist data synced from your clients. The server supports different storage adapters, such as SQLite. Using the `createServer` function, you can create and configure a new server instance: ```js filename="run.js" copy const port = +(process.env.PORT || 8080); const startServer = createServer({ storage: 'sqlite', verboseLogs: true, }); const dbServer = startServer(port); console.log('running on port', port); process.on('SIGINT', function () { dbServer.close(() => { console.log('Shutting down server... '); process.exit(); }); }); ``` You can now deploy the server to a cloud provider that supports Git deploys, like [Vercel](https://vercel.com/docs/git), [Netlify](https://docs.netlify.com/configure-builds/get-started/), or [Render](https://docs.render.com/deploys). ### Storage Triplit is designed to support any storage that can implement a key value store. You may specify the storage adapter you want to use by setting the `storage` option. Triplit provides some default configurations of our storage adapters, which you can use by setting the `storage` option to the appropriate string value for the adapter. These include: - `memory` (default) - See `memory-btree` - `sqlite` - An SQLite storage adapter, which requires the installation of the [`better-sqlite3` package](https://github.com/WiseLibs/better-sqlite3) - `lmdb` - An LMDB storage adapter, which requires the installation of the [`lmdb` package](https://github.com/kriszyp/lmdb-js) In-memory storage adapters are not durable and are not recommended for production use. Typically this will use your `LOCAL_DATABASE_URL` environment variable so you'll want to make sure that's set. You can also pass in an instance of an adapter or a function that returns an instance of an adapter. ```typescript function createAdapter() { return new MyCustomAdapter(); } const startServer = createServer({ storage: createAdapter, }); ``` ## Health checks The server exposes a health check endpoint at `/healthcheck`. This endpoint will return a 200 status code if the server is running and healthy. ## Secrets There are a few secrets that you need to provide to the server to enable certain features. **If you are planning on using the Triplit Dashboard, you will need to set `JWT_SECRET` to the global Triplit public RSA key associated with your project.** Read the [Triplit Cloud guide](/triplit-cloud/self-hosted-deployments#configuration) for more information. ### `JWT_SECRET` The server uses JWT tokens to authenticate clients, and you need to provide a symmetric secret or public key to verify these tokens that it receives. The `JWT_SECRET` environment variable should be assigned to this validation secret. Triplit supports both symmetric (HS256) and asymmetric (RS256) encryption algorithms for JWTs. You will need to generate client tokens signed with the appropriate algorithm. You can generate tokens with the `jsonwebtoken` package (e.g. if you wanted to use asymmetric encryption) : ```typescript copy const anonKey = jwt.sign( { 'x-triplit-token-type': 'anon', }, process.env.PUBLIC_KEY, ); const serviceKey = jwt.sign( { 'x-triplit-token-type': 'secret', }, process.env.PUBLIC_KEY, ); ``` For more complicated authentication schemes, refer to our [authentication guide](/auth). ### `LOCAL_DATABASE_URL` (required for durable storage) An absolute path on the server's file system to a directory where the server will store any database files. This is required for durable storage options: `lmdb`, and `sqlite`. ### `EXTERNAL_JWT_SECRET` (optional) If you plan to connect your self-hosted Triplit server to the Triplit Dashboard and use JWTs for authentication and permission within Triplit, EXTERNAL_JWT_SECRET should only be set on your Triplit Dashboard. Ensure the env var is NOT included wherever you deployed your Docker image. Otherwise, you may encounter errors related to invalid JWTs and JWT signatures. If you want your server to support JWTs signed by a second issuer, you can also set `EXTERNAL_JWT_SECRET` to that signing secret (or public key). For the server to recognize a JWT as "external", it must **not** have the `x-triplit-token-type` claim or if that claim is set, it must **not** have the value of `anon` or `secret`. Those specific combinations of claims are reserved for "internal" JWTs, e.g. the special `anon` and `secret` tokens. ### `CLAIMS_PATH` (optional) If you are using custom JWTs with nested Triplit-related claims, you can set the `CLAIMS_PATH` environment variable. The server will read the claims at the path specified by `CLAIMS_PATH`. Read the [authentication guide](/auth) for more information. ### `SENTRY_DSN` (optional) If you want to log errors to Sentry, you can set the `SENTRY_DSN` environment variable. The server will automatically log errors to Sentry. ### `VERBOSE_LOGS` (optional) If you want to log all incoming and outgoing messages and requests, you can set the `VERBOSE_LOGS` environment variable. This can be useful for debugging. --- ## ssr # Server-side rendering Triplit is designed to work in a client-side environment, but it can work in a server-side rendering (SSR) environment as well. ## The HTTP client When working with Triplit data in a server environment (e.g. to hydrate a pre-rendered page), the `TriplitClient`'s default query and mutation methods will not work. They rely on establishing a sync connection over `WebSockets`, which is not possible in many stateless server-rendering environments. Instead, use the [`HttpClient`](/client/http-client), a stateless Triplit client that can perform operations on a remote Triplit server over HTTP. It is fully-typed and has a broadly similar API to the core Triplit Client. ```ts filename="server-action.ts" // This code runs on the server const httpClient = new HttpClient({ serverUrl: PUBLIC_TRIPLIT_URL, token: PUBLIC_TRIPLIT_TOKEN, }); const results = await httpClient.fetch(httpClient.query('allPosts')); ``` ## Client configuration Though we recommend only using the [`HttpClient`](/client/http-client) to _fetch or mutate data_ in a server-rendering environment, a `TriplitClient` can be instantiated in code that runs on a server with some specific configuration. You will often want to do this if you have a single `TriplitClient` instance that is shared between server and client code. ### WebSockets and auto-connect By default, a new client attempts to open up a sync connection over [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) with the provided `serverUrl` and `token`. This auto-connecting behavior is controlled with the `autoConnect` client parameter. If you are instantiating a client in code that may run in an environment where WebSockets are not available (e.g. during server-side rendering), you should set `autoConnect` to `false` or preferably to an [environmental variable](https://kit.svelte.dev/docs/modules#$app-environment-browser) that indicates whether the client should connect. Allowing the client to attempt to connect to the server over WebSockets in a server-side rendering environment will result in an error or undefined behavior. Here's an example in SvelteKit: ```ts filename="src/lib/client.ts" export const client = new TriplitClient({ serverUrl: PUBLIC_TRIPLIT_URL, token: PUBLIC_TRIPLIT_TOKEN, autoConnect: browser, }); ``` ### Storage You may chose to use a storage provider like IndexedDB to provide a durable cache for your client. IndexedDB is not available in a server-side rendering environment, so you should use a different storage provider in that case. Attempting to use IndexedDB in a server-side rendering environment will result in an error or undefined behavior. Continuing the SvelteKit example: ```ts filename="src/lib/client.ts" export const client = new TriplitClient({ serverUrl: PUBLIC_TRIPLIT_URL, token: PUBLIC_TRIPLIT_TOKEN, autoConnect: browser, storage: browser ? 'indexeddb' : 'memory', }); ``` ## Looking ahead In the future, we plan to provide a more robust solution for server-side rendering with Triplit. Keep an eye on [our roadmap](https://triplit.dev/roadmap) and [Discord](https://triplit.dev/discord) to stay updated. --- ## v1-migration # Migrating to Triplit 1.0 Triplit 1.0 is here. It's a major upgrade that includes significant improvements to performance and reliability. There are also several improvements to the API, including: - Simplified query syntax and the removal of `.build()` from the query builder API - More type hinting when defining permissions and relations in a schema. - Easier self-hosted server setup with fewer required environment variables. Because Triplit 1.0 uses a new data storage format and redesigned sync protocol, client and server must be updated in tandem, and neither will be backwards compatible with their pre-1.0 counterparts. The server upgrade involves a data migration. If you're using Triplit Cloud, we'll handle this for you when you're ready to upgrade. If you're self-hosting, you can follow the instructions in the [server upgrade section](#migrating-your-server) below. ## Query builder ### Capitalization and `.build()` Anywhere you have a Triplit query defined in your app, you'll need to make some subtle updates. Every builder method (e.g. `.Where`, `.Select`, `.Include`) is now capitalized. In addition, you no longer need to call `.build()` at the end of your query. Here's an example of a query before and after the upgrade: Before: ```ts const query = triplit .query('todos') .where('completed', '=', false) .order('created_at', 'ASC') .include('assignee') .build(); ``` After: ```ts const query = triplit .query('todos') .Where('completed', '=', false) .Order('created_at', 'ASC') .Include('assignee'); ``` ### `SyncStatus` The `SyncStatus` parameter has been changed from a query builder method to an option on `TriplitClient.subscribe` and `TriplitClient.fetch` and their permutations (e.g. `fetchOne`, `fetchById`). Before: ```ts const unsyncedTodosQuery = triplit.query('todos').syncStatus('pending').build(); const result = triplit.fetch(unsyncedTodosQuery); const unsubscribeHandler = triplit.subscribe(unsyncedTodosQuery, (result) => { console.log(result); }); ``` After: ```ts const unsyncedTodosQuery = triplit.query('todos'); const result = triplit.fetch(unsyncedTodosQuery, { syncStatus: 'pending', }); const unsubscribeHandler = triplit.subscribe( unsyncedTodosQuery, (result) => { console.log(result); }, undefined, { syncStatus: 'pending', } ); ``` ### `subquery` builder method The `.subquery` builder method has been replaced with two new methods: `.SubqueryOne` and `.SubqueryMany`. Previously the `.subquery` method required a `cardinality` parameter to specify whether the subquery was for a single or multiple entities. These new methods are more explicit and provide better type hinting. Before: ```ts const query = triplit .query('todos') .subquery( 'assignee', triplit.query('users').where('name', '=', 'Alice').build(), 'one' ) .build(); ``` After: ```ts const query = triplit .query('todos') .SubqueryOne('assignee', triplit.query('users').Where('name', '=', 'Alice')); ``` ## Schema ### Reorganized schema sections and better type hinting - Relations in your schema are now defined in a `relationships` section. This makes it easier to see at a glance how your data is connected, and provides better type hinting when you're working with your schema. - The `ClientSchema` type has been removed in favor of an `S.Collections` method that gives better type hinting when defining your schema. Here's an example of a schema before and after the upgrade: Before: ```ts const schema = { todos: { schema: S.Schema({ id: S.Id(), text: S.String(), completed: S.Boolean(), assigneeId: S.String(), assignee: S.RelationById('users', '$1.assigneeId'), }), }, users: { schema: S.Schema({ id: S.Id(), name: S.String(), }), }, } satisfies ClientSchema; ``` After: ```ts const schema = S.Collections({ todos: { schema: S.Schema({ id: S.Id(), text: S.String(), completed: S.Boolean(), assigneeId: S.String(), }), relationships: { assignee: S.RelationById('users', '$1.assigneeId'), }, }, users: { schema: S.Schema({ id: S.Id(), name: S.String(), }), }, }); ``` ### New `S.Default.Set.empty()` option The `S.Default.Set.empty()` is a new option for the `default` option in a `Set` attribute. Here's how to use it: ```ts const schema = S.Collections({ todos: { schema: S.Schema({ id: S.Id(), text: S.String(), completed: S.Boolean(), tags: S.Set(S.String(), ), }), }, }); ``` ### Changed type helpers The `EntityWithSelection` type, previously used to extract an entity from the schema with a specific selection, has been replaced with a `QueryResult` type. This new type is more flexible and provides better type hinting when working with your schema. Before: ```ts type UserWithPosts = EntityWithSelection< typeof schema, 'users', // collection ['name'], // selection // inclusions >; ``` After: ```ts type UserWithPosts = QueryResult< typeof schema, } >; ``` ## Client configuration ### `storage` changed The `storage` option in the `TriplitClient` no longer accepts an object with `cache` and `outbox` properties. Instead, you can continue to pass in the simple string values `memory` or `indexeddb`, or in the uncommon case that you are creating your own storage provider, an instance of a `KVStore` (which is a new interface in Triplit 1.0). If you need to specify a name for your IndexedDB database, you can pass in an object with a `type` property set to `'indexeddb'` and a `name` property set to the desired name of your database. Before: ```ts const client = new TriplitClient({ storage: { outbox: new IndexedDBStorage('my-database-outbox'), cache: new IndexedDBStorage('my-database-cache'), }, }); ``` After: ```ts const client = new TriplitClient({ storage: { type: 'indexeddb', name: 'my-database', }, }); // also works if you don't need to specify a name const client = new TriplitClient({ storage: 'indexeddb', }); ``` ### Storage imports If you chose to import storage providers directly, previously our storage providers were only exported from `@triplit/db`, so you needed to install `@triplit/db` alongside `@triplit/client`. Providers are now directly exported by `@triplit/client`. Before: ```ts const client = new TriplitClient({ storage: { outbox: new IndexedDBStorage('my-database-outbox'), cache: new IndexedDBStorage('my-database-cache'), }, }); ``` After: ```ts const client = new TriplitClient({ storage: new IndexedDbStorage('my-database'), }); ``` For most purposes, you should only need to install `@triplit/client`. ### `experimental.entityCache` removed The `experimental.entityCache` option has been removed from the `TriplitClient` configuration. This option is no longer needed in Triplit 1.0. ## Client methods ### Deleting optional attributes Previously, deleting an optional attribute in the `TriplitClient.update` method would remove the key from the entity. Any attribute wrapped in `S.Optional` would be of type `T | undefined`. Now, deleting an optional attribute will set the attribute to `null`, and the attribute will be of type `T | undefined | null`. ### `insert`, `update`, `delete`, `transact` return types changed These methods no longer return a `` object. Instead, if they have an output, e.g. `insert`, they return it directly. Before: ```ts // output is the inserted entity const = await triplit.insert('todos', { id: '1', text: 'Buy milk', }); const = await triplit.update('todos', '1', (e) => { e.text = 'Buy buttermilk'; }); const = await triplit.delete('todos', '1'); ``` After: ```ts const output = await triplit.insert('todos', ); // these methods have no return value await triplit.update('todos', '1', (e) => { e.text = 'Buy buttermilk'; }); await triplit.delete('todos', '1'); ``` Retrying and rollback with `TxId` is no longer necessary, as the sync engine now handles rollbacks with a new API. See below for more details. ### Sync error handling methods changed The `TriplitClient` methods `retry`, `rollback`, `onTxSuccessRemote`, and `onTxFailureRemote` have been replaced with a new API for handling sync errors. The new methods include `onFailureToSyncWrites`, `onEntitySyncError`, `onEntitySyncSuccess`, `clearPendingChangesForEntity` and `clearPendingChangesForAll`. Instead of registering callbacks for a specific transaction, you may register callbacks for a specific entity. It is important that you handle sync errors in your app code, as the sync engine can get blocked if the entity that causes the error is not removed from the outbox or updated. Before: ```ts const = await triplit.insert('todos', ); triplit.onTxSuccessRemote(txId, () => { console.log('Transaction succeeded'); }); triplit.onTxFailureRemote(txId, () => { console.log('Transaction failed'); triplit.rollback(txId); }); ``` After: ```ts const insertedEntity = await triplit.insert('todos', { id: '1', text: 'Buy milk', }); triplit.onEntitySyncSuccess('todos', '1', () => { console.log('Entity synced'); }); // if you need to full rollback triplit.onEntitySyncError('todos', '1', () => { triplit.clearPendingChangesForEntity('todos', '1'); }); // if you can handle the error and want to try with a changed entity // mutating the entity will trigger a new sync triplit.onEntitySyncError('todos', '1', () => { triplit.update('todos', '1', (e) => { e.text = 'Buy buttermilk'; }); }); // if you want to listen to any failed write over sync triplit.onFailureToSyncWrites((error, writes) => { console.error('Failed to sync writes', error, writes); await triplit.clearPendingChangesAll(); }); ``` ### `getSchemaJSON` removed The `getSchemaJSON` method has been removed from the `TriplitClient` API, as the schema is now JSON by default. Before: ```ts const serializedSchema = await triplit.getSchemaJSON(); ``` After: ```ts const serializedSchema = await triplit.getSchema(); ``` ## Frameworks ### Angular Triplit previously maintained two sets of Angular bindings: the signal-based `injectQuery` and the observable-based `createQuery`. In Triplit 1.0, we've removed the signal-based bindings in favor of the more flexible and powerful observable-based bindings. If you're using the signal-based bindings, you'll need to update your app to use the observable-based bindings. Generally this means adopting the [Async pipe syntax](https://v17.angular.io/guide/observables-in-angular#async-pipe) in your templates or by using the [`@angular/rxjs-interop` package](https://angular.dev/ecosystem/rxjs-interop) to translate them to signals. ### Expo / React Native #### Expo SQLite storage provider There's a new storage provider for Expo applications that use the `expo-sqlite` package. You can now use the `ExpoSQLiteKVStore` storage provider to store data on the device. This provider is available in the `@triplit/client` package. ```ts new TriplitClient({ storage: new ExpoSQLiteKVStore('triplit.db'), }); ``` You should use a new name for your SQLite database to avoid conflicts with any legacy Triplit databases on the device. #### @triplit/react-native We moved relevant React Native code to a new `@triplit/react-native` package. This includes various helpers for configuring your app with Expo and Triplit and re-exports the same hooks available in the `@triplit/react` package. You can find updating information on setting up a react project with Triplit [here](/frameworks/react-native). ### HTTP API If you're using the [`HttpClient`](/client/http-client) to interact with the HTTP API, you won't need to make any changes to your code. If you're interacting with the API directly (e.g. with a raw `fetch` call): - The `/fetch` route now returns an flatter JSON payload of query results (`Entity[]`), rather than a JSON object of the shape``. So for this query: ```ts const response = await fetch('https://<project-id>.triplit.io/fetch', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + TRIPLIT_TOKEN, }, body: JSON.stringify({ collection: 'todos', query: { collectionName: 'todos', where: [['completed', '=', false]], }, }), }); ``` Before: ```ts const result = await response.json(); // result = ]] } ``` After: ```ts const result = await response.json(); // result = [] ``` While the query builder methods (.e.g `Where`, `Order`, `Include`) have changed capitalization, keys in a json query payload remain lowercase. ## Migrating your server ### Triplit Cloud If you're on a Triplit Cloud database, you don't need to do anything to upgrade your server. We'll handle the upgrade for you when you're ready to upgrade. Just contact us in the [Triplit Discord](https://discord.gg/triplit) or at [help@triplit.dev](mailto:help@triplit.dev). ### Self hosted If you're self hosting, you'll need to migrate your own database. Triplit provides a few tools that are helpful for this process, but it is up to you to ensure you safely migrate the database for your application's needs. If you are using SQLite for storage (which is the default) and you are re-using the same machine, it is recommended that you delete your existing database file(s) during the migration. These files should exist at the path you have specified by your `LOCAL_DATABASE_URL` variable. This isn't strictly necessary, but highly recommended as it will improve the performance of the database and ensures no data from V0 is carried over. There are a variety of ways you could go about migrating data from V0 to V1, but generally the steps to migrate your server are: 1. If you have outside traffic to your server and want to re-use your machine, we recommend that you disable that traffic during the migration. For example, if you are directly serving a docker container you can change the port that the container is exposed on. Alternatively, you could deploy to a different machine that won't have outside traffic. 2. Take a snapshot of your database with `triplit snapshot create`. 3. If you are re-using your machine, delete your existing database file(s). 4. Pointing to the server you would like to push to, run `triplit snapshot push --snapshot=<snapshotId>`. 5. With an updated schema as described above, run `triplit schema push`. 6. Congratulations, you have successfully migrated your server from V0 to V1! If things seem stable, you may now re-enable traffic to your server. If anything seems incorrect, you should have your data saved in a snapshot. If you would like any help or have any questions, please reach out to us in the [Triplit Discord](https://discord.gg/triplit) or at [help@triplit.dev](mailto:help@triplit.dev). --- ## v2 # Getting Started with Triplit Cloud v2 Triplit Cloud is currently in preview. There may be bugs, breaking changes, and other issues. Please report any issues you encounter or any feedback you have to our [Github](https://triplit.dev/github) or [Discord](https://triplit.dev/discord). To get access to Triplit Cloud, join our [waitlist](https://triplit.dev/waitlist). This tutorial will show you how to deploy a project to Triplit Cloud. It will cover 1. [How to authenticate with Triplit Cloud using the CLI](#authenticate-with-triplit-cloud) 2. [How to deploy a project to Triplit Cloud](#deploy-your-project) 3. [How to monitor your deployment with the Triplit dashboard](#viewing-your-deployments) ## Prerequisites - A Triplit project. If you don't have one, you can create one by following the [Getting Started with Triplit](/getting-started) guide. - The Triplit CLI. You can install it by running `npm install -g @triplit/cli`. ## Authenticate with Triplit Cloud To authenticate with Triplit Cloud, run the following command from inside your project directory: ```bash copy triplit login ``` This will ask you to enter an email, and will send a one-time password to your email. After entering the one-time password, you will be authenticated with Triplit Cloud. If you already have access to the [Triplit Dashboard](https://triplit.dev/dashboard), make sure you enter the same email you used to sign up for the dashboard. You'll also be asked to select an organization or create a new one. If you're part of an organization, you can select it from the list. If you're not part of an organization, you can create a new one. ```stdout Choose an organization to work with: ? Select an organization › - Use arrow-keys. Return to submit. ❯ Create a new organization ``` You can then run `triplit whoami` to verify that you are authenticated. ``` You're logged in as phil@superdev.com You're currently working with the organization Super Dev Team ``` If you want to set up multiple organizations and switch between them, run `triplit org`. ## Create and link to a cloud project If you've used Triplit Cloud in its prior iteration, your existing projects will be marked as "Legacy" in the dashboard and are wholly separate from the deploy flow described here. If you have any questions or need help, please reach out to us on [Discord](https://triplit.dev/discord). To deploy a project to Triplit Cloud, you need to create a cloud project. To create a cloud project, run the following command: ```bash copy triplit project create ``` This will create a project in Triplit Cloud that will be associated with the current organization you're working with. This command will also create a `triplit.config.json` file in your project directory that looks like this: ```json filename="triplit.config.json" { "id": "project-1234", "name": "my-project" } ``` If at any time you want to change the cloud project that your local project is linked to, run `triplit project link`. ## Copy in your environment variables To connect the Triplit Client in your App to the Triplit Cloud project you created, you'll need to have a properly configured `.env` file in your project. You can view the environment variables from the Triplit Cloud project by running the following command: ```bash copy triplit project manage ``` This will print a url to the Triplit Cloud dashboard where you can manage your project. You can then copy the environment variables from the dashboard and paste them into your `.env` file. ## Deploy your project This part's easy: just run `triplit deploy`. This will bundle your project, including it's `./triplit/schema.ts` file, and deploy it to Triplit Cloud. ```bash copy triplit deploy ``` If you've used a previous iteration of Triplit Cloud and are expecting to run various `triplit migrate` commands, these are no longer necessary. If you update your schema, simply run `triplit deploy` again and your changes will be reflected in the cloud. ## Viewing your deployments You can view your deployments in the Triplit Cloud dashboard. To open the dashboard, run the following command again: ```bash copy triplit project manage ``` Go to the "Deployments" tab to see all of your deployments. --- # Auth ## auth/index # Authentication In this guide we'll show you how to identify users connecting to your Triplit server and how to model access control with that information. By the end of the guide, you'll be able to define a schema that allows users to insert and update their own data, but not other users' data: ```typescript filename="schema.ts" export const schema = S.Collections({ blogPosts: { schema: S.Schema({ id: S.Id(), title: S.String(), content: S.String(), authorId: S.String(), }), permissions: { authenticated: { read: , insert: , update: , postUpdate: , delete: , }, }, }, }); ``` ### Choose an authentication provider You'll likely start out your project using the `anon` and `service` tokens provided by default. These are great for getting started, but don't provide any user-specific information that you'll need to model access control. Choose from one of the many authentication providers that issue JWTs: - [Clerk](https://clerk.com/): read the Triplit integration guide [here](/auth/integration-guides/clerk) - [Supabase](https://supabase.com/): read the Triplit integration guide [here](/auth/integration-guides/supabase) - [Auth0](https://auth0.com/) - [Amazon Cognito](https://aws.amazon.com/cognito/) - ...or any other provider that issues JWTs ### Allow your Triplit server to verify JWTs If you're using a third-party authentication provider, you'll need to provide the public key or secret that it's using to sign JWTs to your Triplit server so it can verify them. Triplit supports both symmetric (e.g. HMAC) and asymmetric (e.g. RSA) JWT encryption algorithms. If you're connecting to a `triplit.io` URL, [you can use the dashboard](https://www.triplit.dev/docs/triplit-cloud/managed-machines#use-the-dashboard). If you're fully self-hosting Triplit (and _not_ pointed at a `triplit.io` URL), you'll need to set the `EXTERNAL_JWT_SECRET` environmental variable to the public key or symmetric secret. ### Pass the token to Triplit Once you have your authentication provider set up in your app and your user is signed in, you'll need to pass the JWT to Triplit. This is done by calling `startSession` on the `TriplitClient` instance. ```typescript const client = new TriplitClient({ serverUrl: 'https://<project-id>.triplit.io', }); function onPageLoad() { // get the user's token from your authentication provider const token = await auth.getToken(); if (token) { if (client.token) { await client.endSession(); } await client.startSession(token); } } ``` Connections to the server are managed with the Sessions API. Read more about it [here](/auth/sessions). ### Add permissions to your schema Access control to data on a Triplit server is defined by permissions in the schema. Permissions can reference the JWT claims that are passed to Triplit. Once you've added permissions, you need to run `npx triplit schema push` to have them take effect on a deployed server (or just restart the development server if you're running it locally.). ```typescript filename="schema.ts" export const schema = S.Collections({ blogPosts: { schema: S.Schema({ id: S.Id(), title: S.String(), content: S.String(), authorId: S.String(), }), permissions: { authenticated: { read: , insert: , update: , postUpdate: , delete: , }, }, }, }); ``` These permissions will allow any authenticated user to read all blog posts, but only allow them to insert, update, and delete their own posts. Notice that permissions are _defined as query filters_. This means that a permissions can be as complex as a query, and use `or`, `exists` and other query operators. Permissions can also reference relationships and/or any of the JWT claims on the user's `$token`. In this case, we're using the `sub` claim to identify the user. This is the default claim that most authentication providers will use to identify the user. ### Use token claims when inserting or updating data When Triplit is given a JWT token, it makes the decoded claims available through the `TriplitClient.vars.$token`. If your collections have fields that are set to identifiable information from the token, you can access it there and use it in your calls to `insert` or `update`. Using the `blogPosts` example from above, you can set the `authorId` field to the user's `sub` claim when inserting a new post: ```typescript const client = new TriplitClient({ serverUrl: 'https://<project-id>.triplit.io', }); const token = await auth.getToken(); await client.startSession(token); await client.insert('posts', { title: 'My first blog post', content: 'Hello world!', authorId: client.vars.$token.sub, // set the authorId to the user's id }); ``` ### Add additional roles (optional). Triplit has two default roles: `authenticated` and `anonymous`. The `authenticated` role is used for any user that connect with a JWT that has a `sub` claim. The `anonymous` role is assigned to any client that connects with the Triplit-generated `anon` token. This is a special token that is safe to use on the client side and should be used when no user is logged in. You may find that you need to create additional roles for your application. For example, you may have an admin role that is distinct from a normal user. See the [permissions guide](/schemas/permissions) for more information. ## Debugging authentication If you are having trouble connecting to the server, there are a few things to try: - pass the `logLevel: 'debug'` option to the `TriplitClient` constructor to get more information about the connection process and failures. - confirm that your `serverUrl` is correct and that the server is running. - check that the JWT being issued by your authentication provider is valid and has not expired. You can use [jwt.io](https://jwt.io/) to decode the JWT and check its claims. - check that the your Triplit server is configured to accept the JWT. If you're using a third-party authentication provider, make sure that the public key or secret is set correctly in your Triplit server. If you're using a self-hosted server, make sure that the `EXTERNAL_JWT_SECRET` environmental variable is set. ``` ``` --- ## auth/sessions # Sessions Triplit models connections with the server as sessions. Sessions are created when the client is initialized with a `token`, or by calling `startSession`. They end when `endSession` is called. When using durable storage like IndexedDB, they persist through page reloads, and when the client loses connection to the server. This ensures that the server sends only the changes that the client missed while it was disconnected. Triplit client sessions are easy to map to user authentication sessions, but they don't have to. They can also represent different states of the client, like a session for a guest user and a session for a logged-in user. ## `startSession` You have two options to start a session: when you initialize the client or by calling `startSession`. You usually want to initialize your client as early as possible in the lifecycle of your app, which may be before you have a token. In this case, you can create a Triplit Client without a token and then later call `startSession` when you have one. ```ts const client = new TriplitClient({ serverUrl: 'https://<project-id>.triplit.io', }); await client.startSession('your-token'); ``` or ```ts const client = new TriplitClient({ serverUrl: 'https://<project-id>.triplit.io', token: 'your-token', }); ``` You can can also decide whether or not the client should `autoConnect` to the server when you start a session. If you set `autoConnect` to `false`, you can manually connect with the `client.connect()` method. ```ts await client.startSession('your-token', false); // time passes client.connect(); ``` `TriplitClient.startSession` will automatically end the previous session if one is already active. ### Refreshing a session Most authentication providers issue tokens that expire after a certain amount of time. Triplit servers will close the connection with a client when they detect that its token has expired. To prevent this, and keep the connection open, you can provide a `refreshHandler` to the client. The `refreshHandler` is a function that returns a new token, or `null`. The client will call this function 1 second before the token expires, as determined from the `exp` claim. ```ts const client = new TriplitClient({ serverUrl: 'https://<project-id>.triplit.io', }); await client.startSession('your-token', true, { refreshHandler: async () => { // get a new token return await getFreshToken(); }, }); // or in the constructor const client = new TriplitClient({ serverUrl: 'https://<project-id>.triplit.io', token: 'your-token', refreshOptions: { refreshHandler: async () => { // get a new token return await getFreshToken(); }, }, }); ``` You can also provide an `interval` to the `refreshOptions` to set the time in milliseconds that the client will wait before calling the `refreshHandler` again. You should do this if you know the token's expiration time and want more control over when it gets refreshed. ```ts await client.startSession('your-token', true, { interval: 1000 * 60 * 5, // 5 minutes refreshHandler: async () => { // get a new token return await getFreshToken(); }, }); ``` If you want even more granular control over when the client refreshes the token, you can call `updateSessionToken` with a new token. ```ts const client = new TriplitClient({ serverUrl: 'https://<project-id>.triplit.io', token: 'your-token', }); // later await client.updateSessionToken('your-new-token'); ``` It's important that tokens used to refresh a session have the same roles as the original token. They are intended to represent that same user, with the same permissions, as interpreted by roles assigned it by the server and its schema. Read more on roles [here](/schemas/permissions). If you attempt to update the token with a token that has different roles, the server will close the connection and send a `ROLES_MISMATCH` error. ## `endSession` When a user logs out, you should end the session. This will close the connection to the server, cleanup any refresh events, and clear some metadata about the session. ```ts const client = new TriplitClient({ serverUrl: 'https://<project-id>.triplit.io', token: 'your-token', }); // when ready to end session await client.endSession(); // If signing out, it is recommended to also clear your local database await client.clear(); ``` Calling `endSession` **will not** clear the the client's database. If you want to clear the cache, you should call `client.clear()`. ## `onSessionError` `onSessionError` is a function that is called when the client receives an error from the server about the session, which will lead to the sync connection to being terminated. This can be used to end the session, restart it, and/or clear the cache. Potential error cases include: - `UNAUTHORIZED`: the token can't be verified, either because it is expired, is signed with the wrong key, or otherwise unable to be parsed. This indicates that the client has been given an erroneous token or, if you're certain that the token is valid, that the server is misconfigured. - `SCHEMA_MISMATCH`: the schema of the client and the server are out of sync. - `TOKEN_EXPIRED`: the previously valid token that had been used to authenticate the client has expired and the client will no longer receive messages. This message is sent not at the exact time that the token expires, but when the client attempts to send a message to the server or vice versa and the server detects that the token has expired. - `ROLES_MISMATCH`: occurs when the client attempts to update the token with the `refreshHandler` option for `startSession` or when using `updateSessionToken`. This error is sent when the client attempts to update the token with a token that has different roles than the previous token. This is a security feature to prevent a user from changing their privileges by updating their token with one that has different roles. ```ts const client = new TriplitClient({ serverUrl: 'https://<project-id>.triplit.io', token: 'your-token', onSessionError: (type) => { if (type === 'TOKEN_EXPIRED') { // log the user out await client.endSession(); await client.clear(); } }, }); ``` --- ## auth/integration-guides/clerk # Using Clerk with Triplit [Clerk](https://clerk.com) is a user authentication and management service that makes it easy to add user accounts to your application. It's super simple to get Clerk up and running with Triplit, and this guide will show you how. This guide assumes you have a Triplit project set up. If you don't have one, you can create one by following the [Quick Start](/quick-start) guide. ### Create a Clerk account and application Follow the official [Clerk documentation](https://docs.clerk.dev/) to create an account and application in their dashboard. ### Get your Clerk public key We also need to configure Triplit to validate the JWT tokens issued by Clerk. To do this, we need the RSA public key for your Clerk application. You can find this in the **API Keys** section of the Clerk dashboard.  Then click on the **Show JWT Public Key** button to reveal the public key.  For local dev, add this to your `.env` file in your Triplit app, making sure to remove any newlines: ```env copy TRIPLIT_EXTERNAL_JWT_SECRET=-----BEGIN PUBLIC KEY-----<some-encoded-stuff>-----END PUBLIC KEY----- ``` ### Start the Triplit development server Now that we've configured Clerk and added the necessary environmental variables, we can start the Triplit development server: ```bash npx triplit dev ``` ### Add Clerk to your Triplit app You can add Clerk to your Triplit app by installing the appropriate SDK. Clerk has official support for [Vanilla JavaScript](https://clerk.com/docs/quickstarts/javascript), [React](https://clerk.com/docs/quickstarts/react), [Next.js](https://clerk.com/docs/quickstarts/nextjs), [Expo](https://clerk.com/docs/quickstarts/expo), and more. There are also community SDKs for frameworks like [Svelte](https://github.com/markjaquith/clerk-sveltekit), [Vue](https://vue-clerk.vercel.app/), and [Angular](https://github.com/anagstef/ngx-clerk?tab=readme-ov-file#ngx-clerk). See the [Clerk documentation](https://clerk.com/docs) for the full list of integrated frameworks. ### Add the Clerk token to your Triplit client Your Triplit client needs to send the JWT token issued by Clerk with each request. You can do this with the `startSession` method for the `TriplitClient`. Here's an example with Clerk's React bindings: ```ts copy function App() { const = useAuth(); // Refresh the Triplit session when auth state changes useEffect(() => { if (isLoaded) { getToken().then((token) => { if (!token) { client.endSession(); return; } client.startSession(token, true, { refreshHandler: getToken, }); }); } }, [getToken, isLoaded]); } ``` You'll note that we're using the `refreshHandler` option to automatically refresh the token before it expires. Triplit will close down connections with expired tokens, but this will keep connections open for as long as Clerk issues a token that has consistent [roles](/schemas/permissions#roles) . ### Test locally Run you Triplit app and sign in with Clerk. If you’re setup correctly, you should see the connection is established and your data is syncing with your server. If you can't connect, ensure that you set the `TRIPLIT_EXTERNAL_JWT_SECRET` environmental variables correctly. ### Configure your Triplit dashboard To use Clerk with a deployed Triplit server, you just need ensure that it can use the Clerk public key to verify incoming requests. If you're using the Triplit Dashboard, you can add the public key to the **External JWT secret** input in your project settings.  If you're using a custom self-hosted server, you need to set the `EXTERNAL_JWT_SECRET` environmental variable to the public key. Now that you have a user auth system set up, you can add permissions to your Triplit schema. By default, the JWT issued by Clerk will set the `sub` claim to the authenticated user's unique identifier. The server will apply the default `authenticated` role the token. For a given collection, you can define a permission that only allows any authenticated user to read all posts but only mutate their own. ```ts copy filename=schema.ts export const schema = S.Collections({ posts: { schema: S.Schema({ id: S.Id(), title: S.String(), content: S.String(), authorId: S.String(), }), permissions: { authenticated: { read: , insert: , update: , delete: , }, }, }, }); ``` When creating `posts`, you should ensure that the `authorId` is set to the `sub` claim, either taken from the token or the Supabase session. If you need to model more complex roles or permissions, [consult the documentation](/schemas/permissions). --- ## auth/integration-guides/supabase # Using Supabase Auth with Triplit [Supabase Auth](https://supabase.com/docs/guides/auth) is one of the services in the Supabase ecosystem that makes it easy to manage user accounts and authentication in your application. This guide will show you how to set up Supabase Auth with Triplit. This guide assumes you have a Triplit project set up. If you don't have one, you can create one by following the [Quick Start](/quick-start) guide. ### Create a Supabase account and project Use the [Supabase dashboard](https://supabase.com/dashboard) to create an account and a project. This will setup a Postgres database and a Supabase Auth instance for your project. ### Get the JWT Secret for your Supabase project We also need to configure Triplit to validate the JWT tokens issued by Supabase. To do this, we need the JWT signing secret for your Supabase project. You can find this in the **JWT Settings** panel section of the **API Settings** panel.  For local dev, add this to your `.env` file in your Triplit app: ```env copy TRIPLIT_EXTERNAL_JWT_SECRET=<supabase-jwt-secret> ``` ### Start the Triplit development server Now that we've configured Supabase and added the necessary environmental variables, we can start the Triplit development server: ```bash npx triplit dev ``` ### Add Supabase Auth to your Triplit app Your app should have some flow to [authenticate a user](https://supabase.com/docs/reference/javascript/auth-signinwithpassword) that uses Supabase's authentication methods. Once a user is signed in, your Triplit client needs to send the JWT token issued by Supabase with each request, and handle other authentication events. ```ts copy const supabase = createClient( 'https://<project>.supabase.co', '<your-anon-key>' ); const triplit = new TriplitClient({ schema, serverUrl: 'http://localhost:6543', }); function getSessionAndSubscribe() { const = supabase.auth.onAuthStateChange( async (event, session) => { switch (event) { case 'INITIAL_SESSION': case 'SIGNED_IN': await triplit.startSession(session.access_token); break; case 'SIGNED_OUT': await triplit.endSession(); break; case 'TOKEN_REFRESHED': triplit.updateSessionToken(session.access_token); break; } } ); return session.subscription.unsubscribe; } ``` Generally you'll want to run `getSessionAndSubscribe()` when your app loads and then unsubscribe from the session changes when your app unmounts. ```ts copy // on mount const unsubscribe = getSessionAndSubscribe(); // on unmount unsubscribe(); ``` ### Test locally Run you Triplit app and sign in with Supabase Auth. If you’re setup correctly, you should see the connection is established and your data is syncing with your server. If you can't connect, ensure that you set the `TRIPLIT_EXTERNAL_JWT_SECRET` environmental variables correctly. ### Configure your Triplit dashboard To use Supabase Auth with a deployed Triplit server, you just need ensure that it can use the Supabase JWT Secret to verify incoming requests. If you're using the Triplit Dashboard, you can add the JWT Secret to the **External JWT secret** input in your project settings.  If you're using a custom self-hosted server, you need to set the `EXTERNAL_JWT_SECRET` environmental variable to the public key. ### Add access control to your schema Now that you have a user auth system set up, you can add permissions to your Triplit schema. By default, the JWT issued by Supabase will set the `sub` claim to the authenticated user's unique identifier. The server will apply the default `authenticated` role the token. For a given collection, you can define a permission that only allows any authenticated user to read all posts but only mutate their own. ```ts copy filename=schema.ts export const schema = S.Collections({ posts: { schema: S.Schema({ id: S.Id(), title: S.String(), content: S.String(), authorId: S.String(), }), permissions: { authenticated: { read: , insert: , update: , delete: , }, }, }, }); ``` When creating `posts`, you should ensure that the `authorId` is set to the `sub` claim, either taken from the token or the Supabase session. If you need to model more complex roles or permissions, [consult the documentation](/schemas/permissions). --- # Cli ## cli/index # Installing the Triplit CLI Install the CLI in your project: <Tab> ```bash copy npm install --save-dev @triplit/cli ``` </Tab> <Tab> ```bash copy pnpm add --save-dev @triplit/cli ``` </Tab> <Tab> ```bash copy yarn add --dev @triplit/cli ``` </Tab> <Tab> ```bash copy bun add --dev @triplit/cli ``` </Tab> All commands in the CLI can be inspected by adding the `--help` flag. For example, to see the available commands: ```bash triplit --help ``` --- ## cli/clear # `triplit clear` ``` triplit clear Clears the sync server's database Flags: --full, -f Will also clear all metadata from the database, including the schema. --token, -t Service Token --remote, -r Remote URL to connect to ``` --- ## cli/dev # triplit dev ``` triplit dev Starts the Triplit development environment Flags: --storage, -s Database storage type Options: file, leveldb, lmdb, sqlite, memory, memory-array, memory-btree --dbPort, -d Port to run the database server on --verbose, -v Verbose logging --initWithSchema, -i Initialize the database with the local schema --seed, -S Seed the database with data --schemaPath, -P File path to the local schema file --noSchema, -N Do not load a schema file ``` --- ## cli/init # `triplit init` ``` triplit init Initialize a Triplit project Flags: --framework, -f Frontend framework helpers to install Options: react, svelte, vue, angular --template, -t Project template to use Options: chat ``` --- ## cli/repl # triplit repl ``` triplit repl Start a REPL with the Triplit client Flags: --token, -t Service Token --remote, -r Remote URL to connect to --schemaPath, -P File path to the local schema file --noSchema, -N Do not load a schema file ``` --- ## cli/roles # `triplit roles` ## `triplit roles eval` ``` triplit roles eval See what roles the given token is allowed to assume Arguments: 0: token A JWT token or a JSON-parseable string of claims Flags: --location, -l Location of the schema file Options: local, remote --token, -t Service Token --remote, -r Remote URL to connect to --schemaPath, -P File path to the local schema file --noSchema, -N Do not load a schema file ``` --- ## cli/schema # `triplit schema` Read our guide on [how to update a schema](/schemas/updating) for more information. ## `triplit schema push` ``` triplit schema push Apply the local schema to the server Flags: --failOnBackwardsIncompatibleChange Fail if there is a backwards incompatible change --printIssues Print all schema warnings regardless of whether the push succeeds or not --token, -t Service Token --remote, -r Remote URL to connect to --schemaPath, -P File path to the local schema file --noSchema, -N Do not load a schema file ``` ## `triplit schema print` ``` triplit schema print View the schema of the current project Flags: --location, -l Location of the schema file Options: local, remote (default: remote) --format, -f Format of the output Options: json, typescript, json-schema (default: typescript) --token, -t Service Token --remote, -r Remote URL to connect to --schemaPath, -P File path to the local schema file --noSchema, -N Do not load a schema file ``` ## `triplit schema diff` ``` triplit schema diff Show the diff between local and remote schema Flags: --token, -t Service Token --remote, -r Remote URL to connect to --schemaPath, -P File path to the local schema file --noSchema, -N Do not load a schema file ``` --- ## cli/seed # `triplit seed` Read our guide on [how to seed data](/seeding) for more information. ## `triplit seed create` ``` triplit seed create Creates a new seed file Arguments: 0: filename Name for your seed file Flags: --schemaPath, -P File path to the local schema file --noSchema, -N Do not load a schema file ``` ## `triplit seed run` ``` triplit seed run Seeds a Triplit project with data Arguments: 0: file Run a specific seed file Flags: --all, -a Run all seed files in /triplit/seeds --token, -t Service Token --remote, -r Remote URL to connect to --schemaPath, -P File path to the local schema file --noSchema, -N Do not load a schema file ``` --- ## cli/snapshot # `triplit snapshot` ## `triplit snapshot create` ``` triplit snapshot create Exports all database information to files. Flags: --outDir The directory to save the snapshot to. --token, -t Service Token --remote, -r Remote URL to connect to ``` ## `triplit snapshot push` ``` triplit snapshot push Pushes a snapshot to the server. Flags: --snapshot The directory containing the source snapshot. --token, -t Service Token --remote, -r Remote URL to connect to --ignoreDestructiveWarning Ignore warning that command may be destructive Default: false ``` --- # Client ## client/index # Triplit Client Triplit is most powerful when it is used to sync data across multiple devices and users. Every Triplit Client is equipped with a sync engine that is responsible for managing the connection to the server and syncing data between your local database and remote database. ## Installation ```bash npm install @triplit/client ``` ## Client and Server model As the monicker, "a full-stack database" implies, Triplit has components that run on both the client and the server. In particular, each client has its own database that is fully queryable just like the remote database. Thus, any query can be immediately fulfilled by the client without needing to go to the network. The client database is kept in sync with the remote database by setting up subscriptions to queries. Along with reads, every client is able to write to its database and Triplit ensures the write is propagated to the remote database and other listening clients. Because writes go directly to the client database, your application gets an optimistic effect to all of its writes. ### Client Database On the client, Triplit keeps your data in two storage areas: an `outbox` and a `cache`. The outbox is a buffer of data that is waiting to be sent to the server. The cache is a local copy of the data that has been confirmed to have been received by the server. By default, results from the cache and outbox are merged together, however queries can be configured to only return data from the cache (to display only confirmed data) or only return data from the outbox (to denote unconfirmed data). If a client is offline or is not connected to the remote database for any reason, then the data will remain in the outbox so that data can be synced at a later time. ### Server Database The server database provides durable centralized storage for clients and handles propagating writes to other listening clients. As well, the server acts as an authoritative figure for data as needed. For example, access control rules are checked on the server and unauthorized updates are rejected and not propagated. ## Setting up sync To set up syncing, you need to tell your Triplit Client how to connect to the remote database with the `serverUrl` and `token` options in the constructor. Additional options can be found [here](/client/options). ```typescript const client = new TriplitClient({ serverUrl: '<remote database URL>', token: '<access token>', }); ``` ## Example [Once you've installed](/getting-started) the Triplit CLI, you can run the following command to start a local Triplit Server (and remote database): ```bash npm run triplit dev ``` By default this will set up a server at `http://localhost:6543`, and should display an access token for your database. You can run this script in multiple browser tabs to simulate multiple users/devices connecting and sending data. ```typescript const client = new TriplitClient({ serverUrl: 'http://localhost:6543', token: '<access token>', }); const query = client.query('todos'); client.subscribe(query, (todos) => { console.log(todos); }); client.insert('todos', { text: 'My Todo', createdAt: new Date().toISOString(), }); ``` --- ## client/debugging # Debugging ## Logging Triplit uses a configurable logger to log information about the client and underlying database's state, as well as the results of fetches and mutations. By default the logger is set to `'info'` level, but supports `'warn' | 'error' | 'debug'` as well. You can set the logger level by passing a `logLevel` option to the `TriplitClient` constructor: ```ts const client = new TriplitClient({ logLevel: 'debug', }); ``` If you'd like to save and export logs, you must set the `logLevel` to `'debug'`. You can then access the logs and relevant metadata from the `TriplitClient` instance: ```ts const logs = client.logs; // an array of log objects // copy the logs to the clipboard when in the browser copy(JSON.stringify(logs)); // or save the logs to a file when in Node.js fs.writeFileSync('logs.json', JSON.stringify(logs)); ``` ## Inspecting a client in the browser The `TriplitClient` class can be accessed from your browser console by adding the following code to an application that uses the `TriplitClient`: ```ts copy filename="App.tsx" const client = new TriplitClient(); if (typeof window !== 'undefined') window.client = client; ``` This will make the `client` object available in the browser console, allowing you to inspect the state of the client and call methods on it, e.g. with `client.fetch`. --- ## client/delete # delete `delete()` deletes a record from a collection. For example: ```typescript await client.delete('employee', 'Fry'); ``` --- ## client/event-listeners # Event listeners The `TriplitClient` provides a number of event listeners that you can use to debug your app and remedy sync failures. These listeners are: ## Sync write failures These handlers are called when a write fails to sync with the server because of data corruption, a failed schema check, or insufficient write permissions. ### `onEntitySyncError` Given an the id of an entity and its collection, this handler is called when a write for that entity fails to sync with the server. The callback will provide an error and the specific change for the entity (or accumulation of changes if there have been multiple writes to the entity). It will be called repeatedly until the returned unsubscribe handler is called. You may choose to undo the write by calling `clearPendingChangesForEntity` or retry the write by mutating the entity again. The sync will automatically retry when any write to the entity or any other entity is made. ```typescript copy const insertedEntity = await client.insert('employee', { id: 'Fry', name: 'Philip J. Fry', }); const unsub = client.onEntitySyncError('employee', 'fry', (error, change) => { // Transaction failed on the server // remove the entity from the outbox await client.clearPendingChangesForEntity('employee', 'fry'); }); ``` ### `onFailureToSyncWrites` When registered, the handler will be called when any write fails to sync. The callback provides an error object and the buffer of writes (a nested `Map<Collection,Map<Id,Change>>`) that failed to sync. It will be called repeatedly until the returned unsubscribe handler is called. You may choose to undo the write by calling `clearPendingChangesAll`, or by calling `clearPendingChangesForEntity` for each entity that failed to sync. The sync will automatically retry when any write to the entity or any other entity is made. ```typescript copy const unsub = client.onFailureToSyncWrites((error, writes) => { console.error('Failed to sync writes', error, writes); await client.clearPendingChangesAll(); }); ``` ## Sync write success ### `onEntitySyncSuccess` Given an the id of an entity and its collection, this handler is called when a write for that entity successfully syncs with the server. It will be called repeatedly (e.g. after multiple updates to the same entity) until the returned unsubscribe handler is called. ```typescript copy const insertedEntity = await client.insert('employee', { id: 'Fry', name: 'Philip J. Fry', }); const unsub = client.onEntitySyncSuccess('employee', 'fry', () => { // Insert succeeded on the server }); ``` ## Auth events ### `onSessionError` Read more about this listener in the [Sessions API](/auth/sessions#onsessionerror) documentation. ## Message events The client communicates with the server over WebSockets. You can listen to messages sent and received with the following handlers: ### `onSyncMessageSent` When registered, the provided callback will fire with the message sent to the server. It will be called repeatedly until the returned unsubscribe handler is called. ```typescript copy const unsub = client.onSyncMessageSent((message) => { console.log('Message sent to server', message); }); ``` ### `onSyncMessageReceived` When registered, the provided callback will fire with the message received from the server. It will be called repeatedly until the returned unsubscribe handler is called. ```typescript copy const unsub = client.onSyncMessageReceived((message) => { console.log('Message received from server', message); }); ``` --- ## client/fetch-by-id # fetchById `fetchById()` queries for a single entity by its id and returns the entity if it is found or `null` if it is not. For example: ```typescript await client.insert('employees', ); await client.insert('employees', ); const fry = await client.fetchById('employees', 'Fry', options); // const leela = await client.fetchById('employees', 'Leela', options); // const bender = await client.fetchById('employees', 'Bender', options); // null ``` This is a convenient shorthand for applying a `Where('id', '=', id)` filter and extracting the result from the result set. --- ## client/fetch-one # fetchOne `fetchOne` is similar to `fetch()` but will return only the first entity in the result set or `null` if the result set is empty. For example: ```typescript await client.insert('employees', ); await client.insert('employees', ); const query = client.query('employees'); const result = await client.fetchOne(query, options); // const queryEmpty = client.query('employees').Where('name', '=', 'Bender'); const resultEmpty = await client.fetchOne(queryEmpty); // null ``` This is a convenient shorthand for using the [limit parameter](/query/limit) `.Limit(1)` and extracting the result from the result set. --- ## client/fetch # fetch `fetch()` executes the specified query and returns an array of entities. For example: ```typescript await client.insert('employees', ); await client.insert('employees', ); const query = client.query('employees'); const result = await client.fetch(query, options); // [] ``` ## Fetch options Because a Triplit Client may be dealing with two databases (your local database and remote database), the exact nature of how you would like to query those is customizable. If no options are provided, queries will be fulfilled with the options ``. ### Policy The `policy` option determines how you interact with your local and remote databases. This is distinct from the [syncStatus](/query/sync-status) parameter on a query, which indicates how you wish to query your local database. The following policy types are valid: - `local-first`: (default) This policy will fetch data directly from the local database, however if is determined that the query cannot be fulfilled it will fetch data from the remote database. If the remote database fails to fulfill the query, the cached data is used. - `local-only`: This policy will fetch data directly from the local database and will never go to the network. - `remote-first`: This policy will fetch data from the remote database and update the local database with those results before querying the local database. - `local-and-remote`: This policy will fetch data from the local database and will fetch results from the remote database in the background and update the local database with those results. Optionally you may provide a `timeout` parameter, which informs Triplit to wait `timeout` milliseconds for the remote result to update the local database. `remote-only` has been deprecated and will be removed in a future release. If you need to fetch data from the remote database only, use [`TriplitClient.http.fetch`](/client/http-client). - `remote-only`: This policy will fetch data directly from the remote database and will not update the local database with results. Results using this policy will also not include any data from the local database - notably any data that has been updated locally but not yet synced. This policy is not available on subscriptions. --- ## client/http-client # HTTP clients ## `TriplitClient.http` You can access the HTTP API through the `http` property on the `TriplitClient` instance. It provides methods for fetching, inserting, updating, and deleting entities in your database. Any queries using this API will bypass the local cache and and mutations will not cause optimistic updates. If you have live queries syncing with the remote database, the Remote API will trigger these queries to update once the server confirms the changes. ```ts const client = new TriplitClient({ serverUrl: 'https://<project-id>.triplit.io', token: TRIPLIT_TOKEN, }); // client.http is an instance of HttpClient // Fetch all entities in the "todos" collection const todos = await client.http.fetch({ collectionName: 'todos', }); // Insert a new entity into the "todos" collection, returns the // entity as `output` if successful const insertedEntity = await client.http.insert('todos', { id: '123', title: 'Buy milk', completed: false, }); // Update an entity in the "todos" collection await client.http.update('todos', '123', (entity) => { entity.completed = true; }); // Delete an entity in the "todos" collection await client.http.delete('todos', '123'); // Delete all entities in the "todos" collection await client.http.deleteAll('todos'); // Fetch the entity with the ID "123" in the "todos" collection const todoWithId123 = await client.http.fetchById('todos', '123'); // Fetch just one entity in the "todos" collection const oneTodo = await client.http.fetchOne({ collectionName: 'todos', where: [['completed', '=', false]], }); ``` ## `HttpClient` If you're only interested in talking to Triplit with the Remote API, and forgo local caching and optimistic updates altogether, you can use the `HttpClient` class directly. ```ts const httpClient = new HttpClient({ serverUrl: 'https://<project-id>.triplit.io', token: TRIPLIT_TOKEN, }); ``` --- ## client/insert # insert `insert()` inserts a new record into a collection and returns the inserted record, if successful. For example: ```typescript const insertedEntity = await client.insert('employee', { id: 'Fry', name: 'Philip Fry', }); ``` The `id` field in Triplit is a special field that is used to uniquely identify records and is required on entities. If you do not provide an id, one will be generated for you. The `insert` method may also be used to upsert an existing record when used in a schemaless database or in conjunction with [optional attributes](/schemas/types#optional). For example: ```typescript const schema = S.Collections({ employee: { schema: S.Schema({ id: S.Id(), name: S.String(), title: S.Optional(S.String()), year_hired: S.Optional(S.Number()), }), }, }); await client.insert('employee', ); await client.insert('employee', { id: 'Fry', name: 'Philip J. Fry', title: 'Pizza Delivery Boy', }); await client.insert('employee', { id: 'Fry', name: 'Philip J. Fry', title: 'Package Delivery Boy', year_hired: 3000, }); ``` When using `insert` to upsert an existing record, any defaults on the schema will be applied even if the record already exists. --- ## client/options # Client configuration ## DB options These options define your local cache. - `schema` is a hard coded schema for your local cache - `storage` determines the storage engines for your local cache (see [Storage](/client/storage)) ## Sync options These options define how you want to sync with the server. - `serverUrl` is the url of the server where your project lives e.g. `https://<project-id>.triplit.io` - `autoConnect` determines whether the client should connect to the server immediately upon instantiation. If set to `false`, you can manually connect with the `client.connect()` method. The client connects over [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API), so you if you are instantiating a client in code that may run in an environment where WebSockets are not available (e.g. the server during server-side rendering), you should set `autoConnect` to `false` or preferably to an [environmental variable](https://kit.svelte.dev/docs/modules#$app-environment-browser) that indicates whether the client should connect. The `serverUrl` can be updated with the `client.updateServerUrl` method. ## Auth options These options define how you authenticate with your client database and remote database. - `token` is a jwt that is used to identify the user to the client database and remote databases (see the [Auth guide](/auth)). - `claimsPath` is the path to the Triplit claims on the token. It should be a `.` separated string like `path.to.claims`. This should match the value set on your project in the Triplit Dashboard. - `onSessionError` is a function that is called when the client receives an error from the server about the session, which will lead to the sync connection to being terminated. Read more about refreshing a session in the [Sessions guide](/auth/sessions). - `refreshOptions` can be used to set the `refreshHandler` and `interval` for the client. Read more about refreshing a session in the [Sessions guide](/auth/sessions). --- ## client/storage # Storage Each client has the option for how they would like to store their data. ## Memory If you would only like to store data ephemerally, you can use the memory storage engine. This will store your data in memory and will not persist across page refreshes. If you use memory storage, data may be lost if the user refreshes the page before the data is sent to the server. If no storage options are provided this is the default. ```typescript const client = new TriplitClient({ storage: 'memory', }); ``` ## IndexedDB If you would like to persist data between refreshes in the browser you can use the IndexedDB storage engine. This will store your data in the browser's IndexedDB database. ```typescript const client = new TriplitClient({ storage: 'indexeddb', }); // Or provide your own name const client = new TriplitClient({ storage: { type: 'indexeddb', name: 'my-database', }, }); ``` Note that passing `` to the constructor will create an IndexedDB databases with the default name `triplit`. If your application connects to multiple projects with separate clients, we recommend you create your own custom-named instances of `IndexedDBStorage` so that naming conflicts do not occur. You can inspect the IndexedDB database in the [browser's developer tools](https://developer.chrome.com/docs/devtools/storage/indexeddb). ### Caching By default, the `TriplitClient` will cache all IndexedDB data in memory for fast access (10x faster than native IndexedDB, in some cases). If you want to disable this, you can set the `cache` option to `false` when creating the `TriplitClient`. ```typescript const client = new TriplitClient({ storage: { type: 'indexeddb', options: , }, }); ``` ## Sqlite ### In the browser Triplit does not currently provide a Sqlite storage adapter for the browser. The Triplit server, which runs in node environments, can use the `sqlite` storage engine. ### In React Native Triplit provides an `expo-sqlite` storage adapter for React Native applications. This adapter uses the [`expo-sqlite`](https://docs.expo.dev/versions/latest/sdk/sqlite/) package to store data on the device. Users of Expo 51 or greater can get started by importing the storage provider from `@triplit/db/storage/expo-sqlite`: ```ts new TriplitClient({ storage: new ExpoSQLiteKVStore('triplit.db'), }); ``` You may also instantiate the storage provider with an existing database instance: ```ts const cacheDB = SQLite.openDatabaseSync('triplit.db'); new TriplitClient({ storage: new ExpoSQLiteKVStore(cacheDB), }); ``` ## Clearing your database To clear a `TriplitClient`'s database, you can call the `clear` method. The clear takes an optional parameter `full` which if set to `true` will clear the entire database including all schema and metadata. If `full` is set to `false` or not provided, only your application data will be cleared. Clearing this data will not sync, so it is useful if you need to purge the data on your client. ```typescript const client = new TriplitClient({ storage: 'indexeddb', }); const full: boolean = ...; async function clearCache() { await client.clear(); } ``` If you are clearing your cache because a user is signing out, it is recommended to use this in conjuction with `client.endSession()` as explained in the [auth guide](/auth#modeling-sign-in-and-sign-out-flows). --- ## client/subscribe-background # subscribeBackground There are certain situations in which you may want to subscribe to a query from the server without immediately needing the results. `subscribeBackground` cuts down on some of the work that `subscribe` does, by setting up a connection for a given query but not materializing the results for a callback function. The data will still be synced to the local database and accessible via other subscriptions. `subscribeBackground` can support a pattern where you have one large subscription to keep your local database up to date, and then define many local-only subscriptions that you know to be a subset of the larger subscription. This will cut down on traffic to the server and in some cases improve performance. However, it may also lead to more data being synced than is necessary. ```typescript const unsubscribeBackground = client.subscribeBackground( query, // Optional { onFulfilled: () => { console.log( 'server has inserted initial results for the subscription into the local database' ); }, onError: (error) => { console.error('error in background subscription', error); }, } ); ``` --- ## client/subscribe-with-expand # subscribeWithExpand `subscribeWithExpand` is a special type of subscription that is used to fetch a window of data that can programmatically grow in size. It is similar to `subscribe` but it has a few differences: - It expects the query to have an initial `Limit` defined. - It returns a `loadMore(pageSize?: number)` function to fetch more data. By default, it fetches the next page of data based on the initial `limit` defined in the query. ```typescript const = client.subscribeWithExpand( query, (results, info) => { // handle results }, (error) => { // handle error }, // Optional { localOnly: false, onRemoteFulfilled: () => { console.log('server has sent back results for the subscription'); }, } ); ``` --- ## client/subscribe-with-pagination # subscribeWithPagination `subscribeWithPagination` is a special type of subscription that is used to fetch data in pages. It is similar to `subscribe` but it has a few differences: - It expects the query to have a `Limit` defined. - It returns `nextPage` and `prevPage` functions to fetch the next and previous pages of data. - The subscription callback will have `hasNextPage` and `hasPreviousPage` booleans to indicate if there are more pages to fetch. ```typescript const = client.subscribeWithPagination( query, (results, ) => { // handle results }, (error) => { // handle error }, // Optional { localOnly: false, onRemoteFulfilled: () => { console.log('server has sent back results for the subscription'); }, } ); ``` --- ## client/subscribe # subscribe Query subscriptions are the "live" version of a fetch. Subscribing to a query will provide continual updates to the query result based on the state of your local database. If the sync engine is connected to the remote database, subscribing to a query will keep your local database in sync with the remote database as well depending on the options provided to the subscription. Starting a subscription is as simple as defining a query and a callback to run when data has updated: ```typescript const unsubscribe = client.subscribe( query, (results) => { // handle results }, (error) => { // handle error }, // Optional { localOnly: false, onRemoteFulfilled: () => { console.log('server has sent back results for the subscription'); }, } ); ``` If a subscription query fails on the server then syncing for that query will stop. However, the subscription will remain active and updates to the local database will still be available. As well, the syncing of other queries will not be impacted. Although you will not receive updates from the server, updates that you make via [mutations](/client/insert) will be sent. ## Subscription options - `syncStatus`: Describes the set of data from the client's various sources to include in the subscription. Can be `pending`, `confirmed`, or `all`. By default, `syncStatus` is set to `all`. - `localOnly`: If set to `true`, the subscription will only listen to changes in the local database and will not attempt to sync with the remote database. Multiple small `localOnly` subscriptions are often paired with a large [background subscription](/client/subscribe-background). By default, `localOnly` is set to `false`. - `onRemoteFulfilled`: A callback that is called when the server has first sent back results for the subscription. --- ## client/sync-engine # Sync Engine ## Connecting and disconnecting Instantiating a client will automatically connect to the server. If the connection is unexpectedly lost, the client will automatically reconnect. You may also manually manage the state connection to the server by calling `client.connect()` and `client.disconnect()`. The connection parameters used in the client constructor can be updated by using the Sessions API. Read more about it in the [auth guide](/auth) ## Connection status The connection status is available at `client.connectionStatus`. You may also listen to changes in the connection status by adding a listener to `client.onConnectionStatusChange(callback, runImmediately)`. If the optional parameter `runImmediately` is set to `true`, the callback will be run immediately with the current connection status. ```typescript client.onConnectionStatusChange((status) => { if (status === 'OPEN') console.log('Connected to server'); if (status === 'CLOSED') console.log('Disconnected from server'); }, true); ``` ## Handling errors Read the [event listeners](/client/event-listeners) documentation for more information on how to handle errors over sync. --- ## client/transact # transact Transactions are a way to group multiple mutations into a single atomic operation. If any part of the mutations fail **on the client**, the entire transaction is rolled back. If the transaction succeeds on the client but fails on the server, it will need to be handled. Read the [event listeners](/client/event-listeners) documentation for more information on how to handle these cases. A transaction can be created by calling `client.transact()`. For example: ```typescript await client.transact(async (tx) => { await tx.insert('employee', ); await tx.insert('employee', ); await tx.insert('employee', { id: 'Bender', name: 'Bender Bending Rodriguez', }); }); ``` --- ## client/update # update `update()` updates an existing record in a collection. For example: ```typescript await client.update('employee', 'Fry', async (entity) => { entity.name = 'Philip J. Fry'; }); ``` If possible, `update` will look at the schema you have provided to provide proper type hints for interacting with you data. If no schema is provided, all fields are treated as `any`. See [here](/schemas/types) for more information on data types. --- ## client/web-worker-client # Web Worker Client Triplit supports running the client in a Web Worker (specifically, a [`SharedWorker`](https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker), which can connect to multiple tabs running a script from the same domain). While running a Web Worker, data syncs between browser tabs without having to sync with server. This reduces network traffic for Triplit apps running in the multiple tabs, move Triplit local database computation to a separate thread, and allow for robust multi-tab offline support. ## `WorkerClient` The `WorkerClient` is a drop-in replacement for the `TriplitClient` that runs in a Web Worker. It provides the same API as the `TriplitClient`. To use it, import `WorkerClient` from `@triplit/client/worker-client` and create a new instance of the client: ```ts copy const client = new WorkerClient({ schema, serverUrl: import.meta.env.VITE_TRIPLIT_SERVER_URL, token: import.meta.env.VITE_TRIPLIT_TOKEN, }); ``` ### With Vite To use it in [Vite](https://vitejs.dev), you need to import an additional parameter `workerUrl`, which helps the Vite build process to correctly bundle the Web Worker: ```ts copy const client = new WorkerClient({ workerUrl, schema, serverUrl: import.meta.env.VITE_TRIPLIT_SERVER_URL, token: import.meta.env.VITE_TRIPLIT_TOKEN, }); ``` **However**, some frameworks, including [SvelteKit](https://kit.svelte.dev), use Vite for development but use platform specific plugins that bundle differently for production. If you encounter issues with the `WorkerClient` in production, try removing the `workerUrl` parameter. Here's an example of how to use the `WorkerClient` in SvelteKit when deploying to Vercel that works in both development and production: ```ts copy export const triplit = new WorkerClient({ workerUrl: dev ? workerUrl : undefined, schema, token: PUBLIC_TRIPLIT_TOKEN, serverUrl: PUBLIC_TRIPLIT_SERVER_URL, autoConnect: browser, }); ``` ## Debugging a `WorkerClient` Because the `WorkerClient` runs in a Shared Worker you can't immediately view the Triplit-specific logs it produces. Instead, navigate to `chrome://inspect/#workers` to view the logs for the Shared Worker. We plan to add better debugging support for the `WorkerClient` in the future. --- # Frameworks ## frameworks/angular # Angular ## New projects The fast way to get started with Triplit is to use Create Triplit App which will scaffold a Angular application with Triplit. Choose `Angular` when prompted for the frontend framework. ## Existing projects If have an existing Angular project, you install the hooks provided by `@triplit/angular`: <Tab> ```bash copy npm i @triplit/angular ``` </Tab> <Tab> ```bash copy pnpm add @triplit/angular ``` </Tab> <Tab> ```bash copy yarn add @triplit/angular ``` </Tab> <Tab> ```bash copy bun add @triplit/angular ``` </Tab> ## Observables ### createQuery The `createQuery` hook return an object with observable properties that contain the results of the query, fetching states and error states. ```ts filename="app.component.ts" @Component({ selector: 'app-root', standalone: true, imports: [TodoComponent, CommonModule], template: ` @if (queryResults.fetching$ | async) { <p>Loading...</p> } @else { <div> @for (todo of queryResults.results$ | async; track todo.id) { } } </div> `, }) export class AppComponent { queryResults = createQuery(() => ({ client: triplit, query: triplit.query('todos').Order('created_at', 'DESC'), })); } ``` ### createInfiniteQuery The `createInfiniteQuery` hook is similar to `createQuery`, but it is used for expanding queries, that is, queries that start with an initial limit but grow beyond that. It returns an object with observable properties that contain the results of the query, fetching states and error states, if there are more available results, and a function to load more data. It's important to define a `limit` in the query to enable the initial pagination. ```ts filename="app.component.ts" @Component({ selector: 'app-root', standalone: true, imports: [TodoComponent, CommonModule], template: ` @if (infiniteQuery.fetching$ | async) { <p>Loading...</p> } @else { <div class="todos-container"> @for (todo of infiniteQuery.results$ | async; track todo.id) { } Load More } </div> `, }) export class AppComponent { infiniteQuery = createInfiniteQuery(() => ({ client: triplit, query: triplit.query('todos').Order('created_at', 'DESC').Limit(5), })); } ``` ### createPaginatedQuery The `createPaginatedQuery` hook is similar to `createQuery`, but it is used for paginated queries. It returns an object with observable properties that contain the results of the query, fetching states and error states, and functions to load the previous and next pages of data. It's important to define a `limit` in the query to enable pagination. ```ts filename="app.component.ts" @Component({ selector: 'app-root', standalone: true, imports: [TodoComponent, CommonModule], template: ` @if (paginatedQuery.fetching$ | async) { <p>Loading...</p> } @else { <div class="todos-container"> @for (todo of paginatedQuery.results$ | async; track todo.id) { } Next page Previous page } </div> `, }) export class AppComponent { paginatedQuery = createPaginatedQuery(() => ({ client: triplit, query: triplit.query('todos').Order('created_at', 'DESC').Limit(5), })); } ``` ### createConnectionStatus The `createConnectionStatus` function returns an observable that emits the current connection status of the client. ```ts filename="app.component.ts" @Component({ selector: 'app-connection-status', standalone: true, imports: [CommonModule], template: ` @if ((status$ | async) === 'CLOSED') { Offline } @else if ((status$ | async) === 'CONNECTING') { Connecting } @else { Online } `, }) export class ConnectionStatusComponent { status$ = createConnectionStatus(triplit); } ``` --- ## frameworks/react-native # React Native React Native is the best way to run Triplit on a mobile app. The hooks available in the [React package](/frameworks/react) are also available in React Native. ## Expo If you are using Expo to setup your React Native project, you can follow these steps to get Triplit up and running. ### 1. Create an Expo project and install Triplit Create your expo project: ```bash npx create-expo-app -t expo-template-blank-typescript cd my-app ``` For more information on setting up an Expo project with typescript see the [Expo documentation](https://docs.expo.dev/guides/typescript/). Next, install Triplit's packages: <Tab> ```bash copy npm i @triplit/client @triplit/react @triplit/react-native npm i @triplit/cli --save-dev ``` </Tab> <Tab> ```bash copy pnpm add @triplit/client @triplit/react @triplit/react-native pnpm add @triplit/cli --save-dev ``` </Tab> <Tab> ```bash copy yarn add @triplit/client @triplit/react @triplit/react-native yarn add @triplit/cli --dev ``` </Tab> <Tab> ```bash copy bun add @triplit/client @triplit/react @triplit/react-native bun add @triplit/cli --dev ``` </Tab> ### 2. Configure polyfills Triplit was originally built to run in web browsers, so a few APIs are used in some core packages and dependencies that are not in the ECMAScript spec that Hermes implements. So you will need to add some polyfills to your project. These polyfills should be imported or implemented in your project's entry file so they can be run as early as possible. Typically this is your `index.js` file. If you are using Expo Router see this [thread](https://github.com/expo/expo/discussions/25122) on creating and using an `index.js` file to add polyfills. ```javascript // Import polyfills relevant to Triplit import '@triplit/react-native/polyfills'; // ... other polyfills // If using Expo Router: import 'expo-router/entry'; // The rest of your entry file ``` ### 3. Use React hooks Triplit's React hooks work just the same in React Native. ```typescript const = useQuery(client, client.query('todos')); ``` ### Additional configuration #### Update metro.config.js (metro \< `0.82.0`) If you are using a Metro version before `0.82.0`, you will need to add a custom Metro config to your project. This is encompasses most users using Expo 52 and below. This is because Triplit uses some features that are not supported by the Metro bundler, notably the [exports](https://nodejs.org/docs/latest-v18.x/api/packages.html#package-entry-points) field. To determine the version of Metro that is installed, run the following command: <Tab> ```bash copy npm list metro ``` </Tab> <Tab> ```bash copy pnpm list metro ``` </Tab> <Tab> ```bash copy yarn list --pattern metro ``` </Tab> <Tab> ```bash copy bun pm ls metro --json ``` </Tab> Below is an example output with version `0.82.3` installed: ```bash $ npm list metro my-app@0.0.1 /path/to/my-app └─┬ react-native@0.79.2 └─┬ @react-native/community-cli-plugin@0.79.2 ├─┬ metro-config@0.82.3 │ └── metro@0.82.3 deduped └─┬ metro@0.82.3 └─┬ metro-transform-worker@0.82.3 └── metro@0.82.3 deduped ``` If you are using a version prior to `0.82.0`, Triplit provides a utility for generating a custom Metro config that will resolve these exports. If you have not already created a `metro.config.js` file, please see the Expo docs on properly [configuring Metro](https://docs.expo.dev/guides/customizing-metro/). Once you have created a `metro.config.js` file, you can add the following code to properly resolve Triplit packages: ```javascript const = require('expo/metro-config'); const config = getDefaultConfig(__dirname); const = require('@triplit/react-native/metro-config'); module.exports = triplitMetroConfig(config); ``` If you would like more control over dependency resolution, you can import `triplitMetroResolveRequest` and use it inside a custom resolver. ```javascript const = require('expo/metro-config'); const config = getDefaultConfig(__dirname); const { triplitMetroResolveRequest, } = require('@triplit/react-native/metro-config'); config.resolver.resolveRequest = (context, moduleName, platform) => { const triplitResult = triplitMetroResolveRequest(moduleName); if (triplitResult) return triplitResult; // Additional resolver logic return context.resolveRequest(context, moduleName, platform); }; module.exports = config; ``` #### Configure Babel (web only) If you are building for the web, you'll need to update a babel configuration file. At the root of your Expo project, create a `babel.config.js` file with the following content: ```javascript module.exports = function (api) { api.cache(true); return { presets: ['babel-preset-expo'], assumptions: { enumerableModuleMeta: true, }, }; }; ``` ### Local development When running a local development server on your machine, it will be running at `localhost`. However if you are running your app on a physical device (ie your phone with the Expo Go app or a custom build) you will need to change the `localhost` to your machine's IP address. You can find your IP address by running `ipconfig getifaddr en0` in your terminal. So a URL `http://localhost:<port>` would become `http://<your-ip>:<port>`. ## Storage providers Triplit provides storage providers for React Native applications to persist data on the device, including for `expo-sqlite`. Read more about the available storage providers in the [client storage documentation](/client/storage#in-react-native). ## Bare React Native The team over at Triplit hasn't had the chance to test out a bare React Native project. Although we don't expect the required steps to be much different than with Expo, there may be differences. If you have set up Triplit in a bare RN project, please let us know how it went! --- ## frameworks/react # React ### New projects The fast way to get started with Triplit is to use Create Triplit App which will scaffold a React application with Triplit. Choose `React` when prompted for the frontend framework. ### Existing projects If you have an existing React project, you can install the hooks provided by `@triplit/react`: <Tab> ```bash copy npm i @triplit/react ``` </Tab> <Tab> ```bash copy pnpm add @triplit/react ``` </Tab> <Tab> ```bash copy yarn add @triplit/react ``` </Tab> <Tab> ```bash copy bun add @triplit/react ``` </Tab> ## useQuery The `useQuery` hook subscribes to the provided query inside your React component and will automatically unsubscribe from the query when the component unmounts. The result of the hook is an object with the following properties: - `results`: An array of entities that satisfy the query. - `fetching`: A boolean that will be `true` initially, and then turn `false` when either the local fetch returns cached results or if there were no cached results and the remote fetch has completed. - `fetchingLocal`: A boolean indicating whether the query is currently fetching from the local cache. - `fetchingRemote`: A boolean indicating whether the query is currently fetching from the server. - `error`: An error object if the query failed to fetch. ```tsx filename="app.tsx" const client = new TriplitClient(); const query = client.query('todos'); function App() { const = useQuery(client, query); if (fetching) return Loading...; if (error) return Could not load data.; return )}</div>; } ``` If you're looking for the most multi-purpose loading state, `fetching` is the one to use. If you want to ensure that you're only showing the most up-to-date data from the server, you can use `fetchingRemote`. If your app is offline and should only wait for the cache, use `fetchingLocal`. ## useQueryOne The `useQueryOne` hook subscribes to a single entity that matches the provided query. You can use this hook inside your React component and it will automatically unsubscribe from updates to the entity when the component unmounts. The result of the hook is the same as the result of `useQuery`, but the `result` property will only have a single the entity or null. ```tsx copy filename="app.tsx" const client = new TriplitClient(); function App() { const = useQueryOne( client, client.query('todos').Id('todo-id') ); return ; } ``` ## usePaginatedQuery The `usePaginatedQuery` hook subscribes to the provided query, and exposes helper functions to load the next or previous page of results. It is useful for patterns that load data in pages, such as paginated lists or content browsing applications. ```tsx copy filename="app.tsx" const client = new TriplitClient(); function App() { const { results, fetchingPage, hasNextPage, hasPreviousPage, nextPage, prevPage, } = usePaginatedQuery( client, client.query('todos').Limit(10).Order('created_at', 'DESC') ); return ( )} </div> ); } ``` For `usePaginatedQuery` to function properly the provided query must have a `limit` set. ## useInfiniteQuery The `useInfiniteQuery` hook subscribes to the provided query, and exposes helper functions for loading more results. It is useful for patterns that continuously load more data in addition to the existing result set. Chat applications or content browsing applications that load more data as the user scrolls are good use cases for `useInfiniteQuery`. ```tsx copy filename="app.tsx" const client = new TriplitClient(); function App() { const = useInfiniteQuery( client, client.query('todos').Limit(10).Order('created_at', 'DESC') ); return ( )} </div> ); } ``` For `useInfiniteQuery` to function properly the provided query must have a `limit` set. By default `loadMore` will increase the limit by the initial limit set in the query. You can also provide a argument to `loadMore` denoting if you want to increment the limit by a different amount. ## useConnectionStatus The `useConnectionStatus` hook subscribes to changes to the connection status of the client and will automatically unsubscribe when the component unmounts. ```tsx copy filename="app.tsx" const client = new TriplitClient(); function App() { const connectionStatus = useConnectionStatus(client); return ( The client is ); } ``` --- ## frameworks/solid # Solid ### New projects The fast way to get started with Triplit is to use Create Triplit App which will scaffold a Vite application with Triplit. Choose `Solid` when prompted for the frontend framework. ### Existing projects If you have an existing Solid project, you can add the hooks provided by `@triplit/solid`: <Tab> ```bash copy npm i @triplit/solid ``` </Tab> <Tab> ```bash copy pnpm add @triplit/solid ``` </Tab> <Tab> ```bash copy yarn add @triplit/solid ``` </Tab> <Tab> ```bash copy bun add @triplit/solid ``` </Tab> ## useQuery The `useQuery` hook subscribes to the provided query inside your Solid component and will automatically unsubscribe from the query when the component unmounts. The result of the hook is an object with the following signal accessor and getter properties: - `results`: An array of entities that satisfy the query. - `fetching`: A boolean that will be `true` initially, and then turn `false` when either the local fetch returns cached results or if there were no cached results and the remote fetch has completed. - `fetchingLocal`: A boolean indicating whether the query is currently fetching from the local cache. - `fetchingRemote`: A boolean indicating whether the query is currently fetching from the server. - `error`: An error object if the query failed to fetch. - `setQuery`: a setter function that can be used to update the query and cleanup the previous query. ```tsx filename="app.tsx" const client = new TriplitClient(); const query = client.query('todos'); function App() { const = useQuery(client, query); return ( <Show when=> <p>Loading...</p> </Show> <Show when=> <div>Could not load data.; </Show> ></div>} </div> ); } ``` ## useConnectionStatus The `useConnectionStatus` hook subscribes to changes to the connection status of the client and will automatically unsubscribe when the component unmounts. ```tsx copy filename="app.tsx" const client = new TriplitClient(); function App() { const = useConnectionStatus(client); return ( The client is ); } ``` --- ## frameworks/svelte # Svelte In anticipation of the release of Svelte 5, Triplit's Svelte bindings use [runes](https://svelte.dev/blog/runes). You must be using one of the pre-release versions of Svelte 5. You can force the compiler into "runes mode" [like this](https://svelte-5-preview.vercel.app/docs/runes#how-to-opt-in). ### New projects The fast way to get started with Triplit is to use Create Triplit App which will scaffold a SvelteKit application with Triplit. Choose `Svelte` when prompted for the frontend framework. ### Existing projects If you have an existing Svelte or Svelte project, you can add the hooks provided by `@triplit/svelte`: <Tab> ```bash copy npm i @triplit/svelte ``` </Tab> <Tab> ```bash copy pnpm add @triplit/svelte ``` </Tab> <Tab> ```bash copy yarn add @triplit/svelte ``` </Tab> <Tab> ```bash copy bun add @triplit/svelte ``` </Tab> ## SvelteKit If you are using SvelteKit, you can use the hooks described below, but you will need to ensure that the client only attempts to connect to the sync server over WebSockets when in the browser. You can do this by checking if the `browser` variable from `$app/environment` is `true`. ```ts filename="src/lib/client.ts" export const client = new TriplitClient({ serverUrl: PUBLIC_TRIPLIT_URL, token: PUBLIC_TRIPLIT_TOKEN, autoConnect: browser, }); ``` The suggested pattern is to create a client instance in a module and import it into your components. ### `vite.config.ts` With the default SvelteKit configuration Vite will not be able to bundle files outside of the `src` or `node_modules` directory. To allow Vite to bundle the files in the `triplit` directory created with `triplit init`, you can add the following configuration to your `vite.config.ts` file: ```ts filename="vite.config.ts" copy export default defineConfig({ plugins: [sveltekit()], server: }, }); ``` ## useQuery The `useQuery` hook subscribes to the provided query inside your Svelte component and will automatically unsubscribe from the query when the component unmounts. The result of the hook is an object with the following properties: - `results`: An array of entities that satisfy the query. - `fetching`: A boolean that will be `true` initially, and then turn `false` when either the local fetch returns cached results or if there were no cached results and the remote fetch has completed. - `fetchingLocal`: A boolean indicating whether the query is currently fetching from the local cache. - `fetchingRemote`: A boolean indicating whether the query is currently fetching from the server. - `error`: An error object if the query failed to fetch. ```svelte filename="App.svelte" const client = new TriplitClient(); let data = useQuery(client, client.query('todos')); <p>Loading...</p> <p>Error: </p> <div> <div> </div> </div> ``` ## useQueryOne `useQueryOne` is like `useQuery` in that it subscribes to the provided query, but adds a `Limit(1)` to the query. It returns the same properties as `useQuery`, except that `results` becomes `result`, which is a single entity or `null` if no entity was found. ## useEntity `useEntity` subscribes to the provided entity in the collection, returning the entity or `null` if the entity does not exist. It returns the same properties as `useQuery`, except that `results` becomes `result`, which is a single entity or `null` if no entity was found. ## useConnectionStatus The `useConnectionStatus` hook subscribes to changes to the connection status of the client and will automatically unsubscribe when the component unmounts. ```svelte copy filename="App.svelte" const client = new TriplitClient({ schema, serverUrl: import.meta.env.VITE_TRIPLIT_SERVER_URL, token: import.meta.env.VITE_TRIPLIT_TOKEN, }); const connection = useConnectionStatus(client); <p>The client is connected</p> <p>The client is not connected</p> ``` --- ## frameworks/tanstack-router # Tanstack Router ### Introduction The `@triplit/tanstack` package provides seamless integration between Tanstack Router and Triplit, allowing you to use a Triplit query as the loader for a route which will automatically be subscribed to and update to both changes from the server as well as optimistic updates from the client. ### Installation To get started make sure you have [Tanstack Router setup](https://tanstack.com/router/latest/docs/framework/react/quick-start) then install the `@triplit/tanstack` package: <Tab> ```bash copy npm i @triplit/tanstack ``` </Tab> <Tab> ```bash copy pnpm add @triplit/tanstack ``` </Tab> <Tab> ```bash copy yarn add @triplit/tanstack ``` </Tab> <Tab> ```bash copy bun add @triplit/tanstack ``` </Tab> ## `triplitRoute` The `triplitRoute` function is an integration between Tanstack Router and the Triplit client. It helps define a route with data fetched from the Triplit client and a component to render based on the fetched data. ```ts triplitRoute(triplitClient: TriplitClient, query: ClientQuery, Component: ReactComponent): RouteConfig; ``` ### Parameters - **`triplitClient`**: The instance of the Triplit client, which is used to query the data from the Triplit API. - **`query`**: A Client Query or function that returns a query to be run using the `triplitClient`. The function receives an object containing the `params` for the route. - **`Component`**: A React component that renders the data fetched by the `triplitClient`. The component receives the following props: - `results`: An array containing the results of the query - `error`: An error if the query's subscription has an error ### Returns `triplitRoute` returns a route configuration object, which can be passed to Tanstack Router's route creation functions such as `createFileRoute`. ### Example Usage Here’s an example of how to use the `triplitRoute` function with the Tanstack Router to define a route for displaying contact details: ```tsx filename="src/routes/$contactId.tsx" export const Route = createFileRoute('/$contactId')( triplitRoute( triplitClient, () => triplitClient .query('contacts') .Where('id', '=', params.contactId) .Limit(1), ContactComponent() { if (results.length === 0) { return Contact not found; } const contact = results[0]; return ( ); } ) ); ``` ### Extending the Route The `triplitRoute` funciton can be extended and overriden with additional Tanstack configuration options by spreading the returned route configuration object. ```tsx filename="src/routes/$contactId.tsx" export const Route = createFileRoute('/$contactId')({ ...triplitRoute(triplitClient, () => triplitClient.query('contacts').Where('id', '=', params.contactId).Limit(1) ), // Additional properties can be added here onError: (error) => { console.error('Error fetching contact:', error); }, }); ``` --- ## frameworks/vue # Vue ### New projects The fast way to get started with Triplit is to use Create Triplit App which will scaffold a Vue application with Triplit. Choose `Vue` when prompted for the frontend framework. ### Existing projects If have an existing Vue project, you install the hooks provided by `@triplit/vue`: <Tab> ```bash copy npm i @triplit/vue ``` </Tab> <Tab> ```bash copy pnpm add @triplit/vue ``` </Tab> <Tab> ```bash copy yarn add @triplit/vue ``` </Tab> <Tab> ```bash copy bun add @triplit/vue ``` </Tab> ## useQuery The `useQuery` hook subscribes to the provided query inside your Vue component and will automatically unsubscribe from the query when the component unmounts. The result of the hook is a [reactive](https://vuejs.org/guide/essentials/reactivity-fundamentals.html#reactive) object with the following properties: - `results`: An array of entities that satisfy the query. - `fetching`: A boolean that will be `true` initially, and then turn `false` when either the local fetch returns cached results or if there were no cached results and the remote fetch has completed. - `fetchingLocal`: A boolean indicating whether the query is currently fetching from the local cache. - `fetchingRemote`: A boolean indicating whether the query is currently fetching from the server. - `error`: An error object if the query failed to fetch. Because `useQuery` uses the `reactive` function from Vue, it can't be destructured in the `setup` block. Instead, you can access the properties directly from the returned object. ```vue filename="App.vue" const state = useQuery(client, client.query('todos')); <div> <h1>Todos</h1> <span v-if="state.fetching">Loading...</span> <span v-else-if="state.error">Error: }</span> <ul v-else-if="state.todos"> </ul> </div> ``` ## useConnectionStatus The `useConnectionStatus` hook subscribes to changes to the connection status of the client and will automatically unsubscribe when the component unmounts. ```vue filename="ConnectionStatus.vue" const connection = useConnectionStatus(client); <div> <span v-if="connection.status === 'OPEN'">🟢</span> <span v-else-if="connection.status === 'CLOSED'">🔴</span> <span v-else>🟡</span> </div> ``` --- # Query ## query/index # Defining queries Data in Triplit is organized into collections, so to define a query you must first specify which collection you want to query. For example, if you want to query the `users` collection, you would write the following: ```typescript const query = client.query('users'); ``` ## Using queries This query object is a builder that allows you to specify (and then reuse) a query. To fetch data you must use a method like [`fetch`](/client/fetch) or [`subscribe`](/client/subscribe), or if you're using a query in a frontend component, you can [use one of the Triplit-provided hooks](/frameworks). --- ## query/include # Include If you have defined a [relation in your schema](/schemas/relations) using `RelationById`, `RelationOne`, or `RelationMany`, you can include those related entities in a query. For example, the following schema defines a relation between `users` and `messages` ```typescript const schema = S.Collections({ users: { schema: S.Schema({ id: S.Id(), name: S.Id(), email: S.String(), }), }, messages: { schema: S.Schema({ id: S.Id(), text: S.String(), sender_id: S.String(), }), relationships: { sender: S.RelationById('users', '$sender_id'), }, }, }); ``` By default, a query on `messages` will not include the `sender` as an attribute. To include the sender, use the `Include` method in the query builder. ```typescript const query = client.query('messages').Include('sender'); /* { id: '1', text: 'hello world!', sender_id: 'bob', sender: , }; */ ``` ## Including multiple relations If a collection has multiple relations, you can select them by chaining multiple `Include` calls. ```typescript const query = client.query('messages').Include('sender').Include('receiver'); ``` ## Aliasing and extending relations You can extend and alias relations with the `Include` method. Given a schema with a relation from `directors` to `films`: ```typescript const schema = S.Collections({ films: { schema: S.Schema({ id: S.Id(), title: S.Id(), rating: S.Number(), directorId: S.String(), }), }, directors: { schema: S.Schema({ id: S.Id(), name: S.String(), }), relationships: { allFilms: S.RelationMany('films', { where: [['directorId', '=', '$1.id']], }), }, }, }); ``` You can write an adhoc query that narrows down a director's films to just their top 3, building off the existing `allFilms` relation. ```typescript const query = client .query('directors') .Include('bestFilms', (rel) => rel('allFilms').Order('rating', 'DESC').Limit(3) ); const result = await client.fetch(query); // And the result will be fully typed // { // id: string; // name: string; // bestFilms: []; // } ``` This is also useful for querying nested data. For example: ```typescript const query = client .query('directors') .Include('allFilms', (rel) => rel('allFilms').Include('actors')); // { // id: string; // name: string; // allFilms: [] }[]; // } ``` The extending query can use any valid query builder method, such as `Order`, `Limit`, `Where`, etc. --- ## query/limit # Limit Many times you will want to limit the number of results returned by a query. To do this, you can use the `Limit` method. For example, the following query will return the 10 most recently created users. ```typescript const query = client .query('users') .Select(['id', 'name', 'email', 'dob']) .Order('created_at', 'DESC') .Limit(10); ``` As a convenience, Triplit also provides a method [fetchOne](/client/fetch-one) to fetch just the first entity of a query result. --- ## query/order # Order To order the results of a query, you can use the `Order` method. This method accepts a list of order clauses as an argument. An order clause is a tuple that takes the form `[attribute, direction]`. `direction` can be either `ASC` or `DESC`. Clauses are applied in the order they are provided. For example the following query will return all users ordered by their creation date in descending order. ```typescript const query = client .query('users') .Select(['id', 'name', 'email', 'dob']) .Order('created_at', 'DESC'); ``` Clauses can be passed to `Order` as a single clause or an array of clauses: - `.Order('created_at', 'DESC')` - `.Order(['created_at', 'DESC'])` - `.Order([['created_at', 'DESC']])` You may use dot notation to order by attributes of a record. ```typescript const query = client.query('users').Order('address.city', 'ASC'); ``` ### Ordering with relations If you are using a schema, you can order by attributes of related entities. For example, the following schema defines a relation between `users` and `messages` ```typescript const schema = S.Collections({ users: { schema: S.Schema({ id: S.Id(), name: S.String(), email: S.String(), }), }, messages: { schema: S.Schema({ id: S.Id(), text: S.String(), created_at: S.Date(), sender_id: S.String(), }), relationships: , }, }); ``` You can then order messages by the name of the sender. ```typescript // Order messages by the name of the sender in ascending order client.query('messages').Order('sender.name', 'ASC'); // Order messages by the name of the sender and then by the created date in descending order client.query('messages').Order([ ['sender.name', 'ASC'], ['created_at', 'DESC'], ]); ``` Ordering with relations is only supported for one-to-one relations, such as `RelationById` or `RelationOne`. ### After You may use the `After` method to specify an entity to start the query from. This is useful for paginating results. You must use `Order` before using `After`. At the moment, `After` only supports a single cursor that corresponds to the first `Order` clause. ```typescript const PAGE_SIZE = 10; const query = client .query('users') .Select(['id', 'name', 'email', 'dob']) .Order('created_at', 'DESC') .Limit(PAGE_SIZE); const = client.fetch(query); const lastEntity = firstPage?.pop(); const secondPageQuery = query.After(lastEntity); const = client.fetch(secondPageQuery); ``` --- ## query/select # Select To specify which attributes you want to return, you can use the `Select` method. This method accepts a list of attribute names for the collection as arguments. ```typescript const query = client.query('users').Select(['id', 'name', 'email', 'dob']); ``` If the type you are selecting is a record, you may also select a specific attribute of the record by using dot notation. The result will be an object with just the selected keys. ```typescript const query = client .query('users') .Select(['id', 'address.street', 'address.city']); // } ``` If you do not call select on a query, all attributes are selected. ## Selecting relationships Refer to the docs on [`Include`](/query/include) to learn how to select relationships in a query. --- ## query/subquery # Subqueries Subqueries can be used to add an ad-hoc query on related data to an entity. [Relations](/schemas/relations) are formalized subqueries that are defined in the schema. You can use the `SubqueryOne` and `SubqueryMany` builder methods to to add any nested query to a Triplit query at runtime, regardless of relations in the schema. For example, the following schema has two collections, `users` and `blogs`, where each blog post has an `author` attribute that references a user: ```typescript const schema = S.Collections({ users: S.Schema({ id: S.Id(), name: S.String(), }), blogs: S.Schema({ id: S.Id(), title: S.String(), text: S.String(), author: S.String(), created_at: S.Date(), }), }); ``` ## `SubqueryMany` To query all blogs with their associated user, you can use the `SubqueryMany` method: ```typescript const query = client.query('users').SubqueryMany( 'userBlogs', // key client // query .query('blogs') .Where(['author', '=', '123']) .Select(['title', 'text']) ); /* A given entity in the result will look like this: { id: '123', name: 'Alice', userBlogs: [ , , ], } */ ``` The return value of the subquery stored at the `userBlogs` key in each entity will be a nested array of blog items. ## `SubqueryOne` `SubqueryOne` is like `SubqueryMany`, but will return the subquery's first match. Instead of a full nested array of results, the key where the `SubqueryOne` stores it results will either be the single result or `null`. The following query will return the text of the most recent blog item created by the user: ```typescript const query = client .query('users') .SubqueryOne( 'mostRecentBlog', client .query('blogs') .Select(['text']) .Where(['author', '=', '123']) .Order('created_at', 'DESC') .Limit(1) ); /* A given entity in the result will look like this: { id: '123', name: 'Alice', mostRecentBlog: , } */ ``` --- ## query/sync-status # syncStatus Triplit's client [storage](/client/storage) is split into two areas - an outbox for unsynced updates and a cache for synced updates. Sometimes you may want to indicate that to a user that data has not yet been saved to the server. To do this, you can use the `syncStatus` option in a subscription. This method accepts a single sync state (`pending`, `confirmed`, `all`) as an argument. For example, a messaging app could use two queries to build message sent indicators. In [React](/frameworks/react): ```tsx const messagesQuery = client.query('messages').Order(['createdAt', 'ASC']); function Messages() { const = useQuery(client, messagesQuery); const = useQuery(client, messagesQuery, { syncStatus: 'pending', }); return ( {allMessages?.map((message) => ( <div> <p></p> <p> {pendingMessages?.find( (pendingMessage) => pendingMessage.id === message.id ) ? 'Sending...' : 'Sent'} </p> ))} </div> ); } ``` --- ## query/variables # Variables Variables in Triplit allow you to pass in preset values into queries. They consist of a scope (to prevent collisions) and a dot (`.`) separated path to reference data in the variable. ## Types of variables ### Query variables Query variables are prefixed with the `query` scope and are accessible just to the query they are defined on. They are defined with the `Vars` method in the query builder. For example: ```typescript const baseQuery = client.query('employees').Where([ ['team', '=', 'Delivery Crew'], ['name', '=', '$query.name'], ]); const fryQuery = baseQuery.Vars(); const leelaQuery = baseQuery.Vars(); ``` This can help prevent writing the same query multiple times with different values. Additionally, you can use query variables to open [selective public access](/schemas/permissions#modeling-selective-public-access) to resources, allowing you to create things like public links to private resources. ### Global variables Global variables are prefixed with the `global` scope and are accessible to all queries in the database. They are defined in the client constructor or via the `updateGlobalVariables` method. For example: ```typescript const client = new TriplitClient( }); let query = client.query('employees').Where('name', '=', '$global.name'); // resolves to 'Philip J. Fry' client.db.updateGlobalVariables(); query = client.query('employees').Where('name', '=', '$global.name'); // resolves to 'Turanga Leela' ``` ### Token variables Token variables are prefixed with the `token` scope and are accessible to all queries in that database session. When [authenticating](/auth) with a Triplit server, the server will assign all claims on the JWT to token variables. ### Role variables When determining access control rules with [roles](/schemas/permissions#roles), you can use role variables to reference values from a client's token in your permission definitions. Role variables are prefixed with the `role` scope. ## Accessing variables `$global` and `$token` variables are accessible on the client instance through the `vars` property. ```typescript const client = new TriplitClient({ variables: , token: '<jwt-token>', }); console.log(client.vars.$global.name); // Philip J. Fry console.log(client.vars.$token.sub); // The subject of the JWT token, if it exists ``` --- ## query/where # Where To filter results based on conditions, you can use the `Where` method. This method accepts a list of clauses as arguments. A clause is a tuple that takes the form `[attribute, operator, value]`. For example the following query will return all registered users. ```typescript const query = client .query('users') .Select(['id', 'name', 'email', 'dob']) .Where('is_registered', '=', true); ``` Clauses can be passed to `Where` as a single clause or an array of clauses: - `.Where('is_registered', '=', true)` - `.Where(['is_registered', '=', true])` - `.Where([['is_registered', '=', true], ...additional clauses])` If multiple clauses are provided, all clauses are joined with a logical AND. However, you may use `or` and `and` methods within the clause array to specify how clauses should be logically grouped and joined. For example the following query will return all registered users who are either an admin or an owner. ```typescript const query = client .query('users') .Select(['id', 'name', 'email', 'dob']) .Where([ [ ['is_registered', '=', true], or([ ['role', '=', 'admin'], ['role', '=', 'owner'], ]), ], ]); ``` You may use dot notation to filter by attributes of a record. ```typescript const query = client.query('users').Where('address.city', '=', 'New York'); ``` If you define relationships in your schema you may also access those via dot notation. Triplit will autocomplete up to 3 levels of depth, but arbitrary depth is supported. ```typescript const query = client .query('test_scores') .Where('class.instructor.name', '=', 'Dr. Smith'); ``` ### Boolean clauses You may also pass in a boolean value as a clause. These statements are particularly useful when defining [permissions](/schemas/permissions). ```typescript const query = client.query('users').Where([true]); // 'true' is a no-op, will return all users const query = client.query('users').Where([false]); // 'false' filters out all results, will return no users ``` ### Exists filters You may also define a filter to check if related data exists. Triplit provides an `exists` method to help build subqueries that reference your schema. ```typescript const schema = S.Collections({ todos: { schema: S.Schema({ id: S.Id(), text: S.String(), assignee_ids: S.Set(S.String()), }), relationships: { assignees: S.RelationMany('users', { where: ['id', 'in', '$assignee_ids'], }), }, }, users: { schema: S.Schema({ id: S.Id(), name: S.String(), team: S.String(), title: S.String(), }), }, }); // Todos where at least one assignee is on the engineering team and has the title of manager const query = client.query('todos').Where( exists('assignees', { where: [ ['team', '=', 'engineering'], ['title', '=', 'manager'], ], }) ); ``` You may be tempted to write the query above as: `.Where('assignees.team', '=', 'engineering').Where('assignees.title', '=', 'manager')`. However, this will instead query for todos where at least one assignee is on the engineering team and where at least one assignee has the title of manager. ### Operators The following operators are available for use in where clauses, organized by data type: #### String Operators - `=`, `!=`: Equality and inequality comparison - `>`, `>=`, `<`, `<=`: Greater than, greater than or equal, less than, less than or equal - `isDefined`: Check if the attribute is defined - `in`, `nin`: Check if value is in or not in a set or array of values ```typescript // String in/nin examples ['triplit', 'in', ['triplit', 'hello']]; // true ['triplit', 'nin', ['triplit', 'hello']]; // false ['triplit', 'in', ['hello', 'world']]; // false ['triplit', 'nin', ['hello', 'world']]; // true ``` - `like`, `nlike`: String pattern matching - `_` matches any single character - `%` matches any sequence of zero or more characters ```typescript // String pattern matching examples ['triplit', 'like', 'triplit']; //true ['triplit', 'like', 'tri%']; // true ['triplit', 'like', 'tr_pl_t']; // true ['triplit', 'like', 'trip']; // true // false ``` - `isDefined`: Check if the attribute is defined #### Number Operators - `=`, `!=`: Equality and inequality comparison - `>`, `>=`, `<`, `<=`: Greater than, greater than or equal, less than, less than or equal - `in`, `nin`: Check if value is in or not in a set or array of values - `isDefined`: Check if the attribute is defined #### Boolean Operators - `=`, `!=`: Equality and inequality comparison - `>`, `>=`, `<`, `<=`: Greater than, greater than or equal, less than, less than or equal - `isDefined`: Check if the attribute is defined #### Date Operators - `=`, `!=`: Equality and inequality comparison - `>`, `>=`, `<`, `<=`: Greater than, greater than or equal, less than, less than or equal - `isDefined`: Check if the attribute is defined #### Set Operators - `has`, `!has`: Check if the set contains or does not contain a value - `isDefined`: Check if the attribute is defined - _the operators of its underlying type_ (e.g. string, number, etc.). This will check if some value in the set matches the operator. For example, if the set is of type string, you can use `like` to check if any of the strings in the set match a pattern. #### Record Operators - `isDefined`: Check if the attribute is defined #### Json Operators Json attributes contain arbitrary JSON data, described [here](/schemas/types#json). Any operator corresponding to the underlying type of the JSON data can be used. For example, if the JSON data contains a string, you can use `like` to check if the string matches a pattern. #### Optional Attributes All optional attributes support the `isDefined` operator, which checks if the specified attribute is defined: ```typescript const query = client.query('profiles').Where('email', 'isDefined', true); ``` ## Id shorthand If you want to query by the entity's ID, you can use the `Id` method as a shorthand for `Where('id', '=', id)`. E.g. ```ts const query = client.query('users').Id('the-user-id'); ``` --- # Runtimes ## runtimes/browser # Triplit in the browser Triplit is the embedded database designed to run in any JavaScript environment, including the browser. Almost every developer will use Triplit in the browser through the `TriplitClient` client library. Read more about the `TriplitClient` in the [client library documentation](/client). ## Supported storage options ### Memory The memory storage option is the default storage option for the `TriplitClient`. It stores all data in memory and is not persistent. This means that all data will be lost when the page is refreshed or closed. ### IndexedDB To persist data in the browser, you can use the `indexeddb` storage option in the `TriplitClient`. This will store all data in the browser's IndexedDB database. By default, the `TriplitClient` will cache all data in memory for fast access. If you want to disable this, you can set the `cache` option to `false` when creating the `TriplitClient`. Read more about IndexedDB storage configuration in [the client library documentation](/client/storage#indexeddb). ## Example ```typescript const client = new TriplitClient({ storage: 'indexeddb', token: '<your token>', serverUrl: 'https://<project-id>.triplit.io', }); ``` --- ## runtimes/bun # Triplit in Bun Triplit can run natively in Bun and persist data using Bun's native `bun:sqlite` package. Triplit provides an [example implementation](https://github.com/aspen-cloud/triplit/tree/main/packages/bun-server) and a [docker image](https://hub.docker.com/r/aspencloud/triplit-server-bun). Read more about self-hosting Triplit in the [self-hosting documentation](/self-hosting). ## Supported storage options ### `bun:sqlite` [`bun:sqlite`](https://bun.sh/docs/api/sqlite) is Bun's SQLite3 driver. It stores all data in a single SQLite database file. This is the recommended storage option for most use cases, as it is fast and easy to set up. ## Dev server You can run the Triplit development server in Bun with the following command: ```bash bunx --bun triplit dev ``` You can add the `-s sqlite` flag to use `bun:sqlite` as the storage option. ## Example ```typescript const port = +(process.env.PORT || 8080); const startServer = await createBunServer({ storage: 'sqlite', verboseLogs: !!process.env.VERBOSE_LOGS, jwtSecret: process.env.JWT_SECRET!, projectId: process.env.PROJECT_ID, externalJwtSecret: process.env.EXTERNAL_JWT_SECRET, maxPayloadMb: process.env.MAX_BODY_SIZE, }); const dbServer = startServer(port); console.log('running on port', port); process.on('SIGINT', function () { dbServer.close(() => { console.log('Shutting down server... '); process.exit(); }); }); ``` --- ## runtimes/cloudflare # Triplit in Cloudflare Workers The Triplit server can be run in the [Cloudflare Workers](https://workers.cloudflare.com/) runtime. This is a great option for deploying a serverless Triplit backend. There are some memory limitations to be aware of, as the Cloudflare Workers runtime has a maximum memory limit of 128MB. This makes the Cloudflare Workers runtime a good option for small to medium-sized applications, or for applications that can be modeled with a single tenant database-per-user. Triplit provides an [example implementation](https://github.com/aspen-cloud/triplit/tree/main/packages/cf-worker-server) using the [Hono](https://hono.dev/) framework. ## Supported storage options ### Durable Objects [Durable Objects](https://developers.cloudflare.com/durable-objects/) are a serverless storage option that is designed to be used with Cloudflare Workers. Triplit uses the latest [SQL API](https://developers.cloudflare.com/durable-objects/api/storage-api/#sql-api) to store and retrieve data in Durable Objects. ## Example ```typescript export class MyDurableObject extends DurableObject { state: DurableObjectState; private appPromise: Promise< Awaited<ReturnType<typeof createTriplitHonoServer>> >; constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); this.state = ctx; // Create the Triplit server this.appPromise = createTriplitHonoServer( { // add any configuration options here jwtSecret: env.JWT_SECRET, // this is the Triplit storage provider for Durable Objects storage: new CloudflareDurableObjectKVStore(this.state.storage), }, // inject the platform-specific WebSocket upgrade function upgradeWebSocket ); } async fetch(request: Request) { // Await the app initialization before handling the request const app = await this.appPromise; return app.fetch(request); } } export default { async fetch(request, env, _ctx): Promise<Response> { // Get the Durable Object ID (this is where you could easily add multi-tenancy) let id: DurableObjectId = env.MY_DURABLE_OBJECT.idFromName('triplitDB'); let stub = env.MY_DURABLE_OBJECT.get(id); // Forward the request to the Durable Object return await stub.fetch(request); }, } satisfies ExportedHandler<Env>; ``` --- ## runtimes/hermes # Hermes (React Native) Triplit's client library can be used in React Native applications built on the [Hermes runtime](https://github.com/facebook/hermes). ## Supported storage options ### Memory The memory storage option is the default storage option for the `TriplitClient`. It stores all data in memory and is not persistent. This means that all data will be lost when the page is refreshed or closed. ### Expo SQLite Triplit provides an `expo-sqlite` storage adapter for React Native applications built with [Expo](https://expo.dev/). This adapter uses the [`expo-sqlite`](https://docs.expo.dev/versions/latest/sdk/sqlite/) package to store data on the device. Read Triplit's [expo-sqlite storage provider documentation](/client/storage#in-react-native) for more information. ## Example ```typescript new TriplitClient({ storage: new ExpoSQLiteKVStore('triplit.db'), serverUrl: process.env.EXPO_PUBLIC_SERVER_URL, token: process.env.EXPO_PUBLIC_TOKEN, }); ``` --- ## runtimes/node # Triplit in Node.js Node is the default runtime for the Triplit server. Triplit provides an [example implementation](https://github.com/aspen-cloud/triplit/tree/main/packages/node-server) and a [docker image](https://hub.docker.com/r/aspencloud/triplit-server). Read more about self-hosting Triplit in the [self-hosting documentation](/self-hosting). ## Supported storage options ### SQLite SQLite is the default storage option for the Triplit server. It stores all data in a single SQLite database file. This is the recommended storage option for most use cases, as it is fast and easy to set up. ### LMDB [LMDB](https://github.com/kriszyp/lmdb-js) is a fast, memory-mapped database that is designed for high performance. ## Example ```typescript const port = +(process.env.PORT || 8080); const startServer = await createServer({ storage: 'sqlite', verboseLogs: !!process.env.VERBOSE_LOGS, jwtSecret: process.env.JWT_SECRET, projectId: process.env.PROJECT_ID, externalJwtSecret: process.env.EXTERNAL_JWT_SECRET, maxPayloadMb: process.env.MAX_BODY_SIZE, }); const dbServer = startServer(port); console.log('running on port', port); process.on('SIGINT', function () { dbServer.close(() => { console.log('Shutting down server... '); process.exit(); }); }); ``` --- # Schemas ## schemas/index # Schemas ## Schemaful vs Schemaless Providing a schema to Triplit is optional, **but it is recommended** in order to take advantage of all the features provided by Triplit. Limitations of schemaless mode include: - You are limited to exclusively using storing value types that are supported by JSON: string, number, boolean, objects, null. - If you use Typescript, you will not get type checking for your queries and results. - [Access control rules](/schemas/rules) are defined in schemas, and thus are not supported in schemaless mode. ## Defining your schema A schema object defines your collections and the attributes and relationships on those collections. Schemas are defined in Javascript like so: ```typescript const schema = S.Collections({ todos: { schema: S.Schema({ id: S.Id(), text: S.String(), complete: S.Boolean(), created_at: S.Date(), tags: S.Set(S.String()), }), }, users: { schema: S.Schema({ id: S.Id(), name: S.String(), address: S.Record({ street: S.String(), city: S.String(), state: S.String(), zip: S.String(), }), }), }, }); const client = new TriplitClient({ schema, }); ``` Passing a schema to the client constructor will override any schema currently stored in your cache. This can cause data corruption if the new schema is not compatible with existing data in the shape of the old schema. Refer to the [schema management guide](/schemas/updating) for more information. By default, your schema file will be created by `triplit init` or `npm create triplit-app` in your project directory at `triplit/schema.ts`. If you need to save your schema file somewhere else, you can specify that path with the `TRIPLIT_SCHEMA_PATH` environmental variable and the Triplit CLI commands will refer to it there. ### id Every collection in Triplit must define an `id` field in its schema. The `S.Id()` data type will generate a random `id` by default upon insertion. If you want to specify the `id` for each entity, you may pass it **as a string** in to the `insert` method as shown below. ```typescript // assigning the id automatically await client.insert('todos', { text: 'get tortillas', complete: false, created_at: new Date(), tags: new Set([groceries]), }) // assigning the id manually await client.insert('todos', { id: 'tortillas' text: 'get tortillas', complete: false, created_at: new Date(), tags: new Set([groceries]), }) ``` ### Getting types from your schema While the `schema` passed to the client constructor will be used to validate your queries and give you type hinting in any of the client's methods, you may want to extract the types from your schema to use in other parts of your application. #### `Entity` You can extract a simple type from your schema with the `Entity` type. ```typescript const schema = S.Collections({ todos: { schema: S.Schema({ id: S.Id(), text: S.String(), complete: S.Boolean(), created_at: S.Date(), tags: S.Set(S.String()), }), }, }); type Todo = Entity<typeof schema, 'todos'>; /* Todo will be a simple type: { id: string, text: string, complete: boolean, created_at: Date, tags: Set<string> } */ ``` #### `QueryResult` If you need more advanced types, e.g. that include an entity's relationships, you can use the `QueryResult` type. It allows you to generate the return type of any query, e.g. with a `Select` clause that narrows fields or `Include` clauses that add related entities. ```ts const schema = S.Collections({ users: { schema: S.Schema({ id: S.Id(), name: S.String(), }), relationships: { posts: S.RelationMany('posts', { where: [['authorId', '=', '$1.id']], }), }, }, posts: { schema: S.Schema({ id: S.Id(), text: S.String(), authorId: S.String(), }), }, }); type UserWithPosts = QueryResult< typeof schema, } >; /* type UserWithPosts = { name: string; posts: Array<{ id: string; text: string; }> } */ ``` ### Reading your schema Your schema is available in your codebase in the `triplit/schema.ts` file. However you may locally edit the schema, or you may not be aware of remote edits that have happened to the schema. To view the current state of the server's schema, run: ```bash triplit schema print -l remote -f file ``` See [CLI docs](/cli/schema#triplit-schema-print) or run `triplit schema print --help` for more options. --- ## schemas/permissions # Authorization and access control Access control checks run exclusively on the server, and are not enforced on the client. Invalid writes will only be rejected when they have been sent to the server. Triplit provides a flexible way to define access control rules for your database, ensuring that your application data is secure without the need for complex server-side logic. ## Roles When a client authenticates with a Triplit server and begins a session, it provides a token that contains some information about itself (see [authentication](/auth) for more information on tokens). The server will assign that token some number of roles based on the claims present in the token. ### Default roles #### `anonymous` The server will assign the `anonymous` role to any client that presents the `anon` token generated in the Triplit dashboard or by the Triplit CLI. You might use this token to allow unauthenticated users to access your database. #### `authenticated` The server will assign the `authenticated` role to any client that presents a token that has a [`sub` claim](https://mojoauth.com/glossary/jwt-subject/). The `sub` or "subject" claim is a standard JWT claim that identifies the principal user that is the subject of the JWT. Tokens with the sub claim should be issued by an authentication provider such as [Clerk](/auth/integration-guides/clerk) or [Supabase](/auth/integration-guides/clerk). Because the `sub` claim is a unique identifier, we can use it to both attribute data to a user and to restrict access to that data to them. #### Example usage You might use the `anonymous` role to allow unauthenticated users to read your database, but restrict inserts and updates to authenticated users. The following example allows any user to read the `todos` collection, but only authenticated users to insert or update todos: ```typescript filename="schema.ts" copy export const schema = S.Collections({ todos: { schema: S.Schema({ id: S.Id(), text: 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']], }, }, }, }, }); ``` ### Custom roles The default roles exist to make it possible to add authorization rules to your database with minimal configuration. However, your app may require more complicated role-based permission schemes than can't be modeled with only the defaults. In that case you can define your own `roles`. Each custom role must have a name and a `match` object. When a client authenticates with a Triplit server, Triplit will check if the token matches any defined roles in the schema. If it does, the client is granted that role and will be subject to any permissions that have been defined for that it. For example, you may author `admin` and `user` tokens with the following structure: ```typescript filename="schema.ts" copy const roles: Roles = { admin: { match: { type: 'admin', }, }, user: { match: { type: 'user', sub: '$userId', }, }, }; ``` Wildcards in the `match` object (prefixed with `$`) will be assigned to [variables](/query/variables) with the prefix `$role`. For example, a JWT with the following structure would match the `user` role and assign the value `123` to the `$role.userId` variable for use in your application's permission definitions: ```typescript // match object { "type": "user", "sub": "$userId", } // Token { "type": "user", "sub": 123 } // Query - resolves to db.query('todos').Where('authorId', '=', 123); db.query('todos').Where('authorId', '=', '$role.userId'); ``` You do not need to assign a token's `sub` claim to a `$role` variable to reference it in a filter. You can access all of the claims on a token directly by using the `$token` variable prefix. e.g. `$token.sub`. Your schema file should export the `roles` object for use in your schema definitions. ### Combining custom and default roles The default roles will only be applied to tokens when your schema has not defined any custom roles. If you define a custom role, the default roles will not be applied to any tokens. If you want to reuse the default and add your own, you can do so with the `DEFAULT_ROLES` constant. ```typescript filename="schema.ts" copy const roles: Roles = { ...DEFAULT_ROLES, admin: { match: { type: 'admin', }, }, user: { match: { type: 'user', sub: '$userId', }, }, }; ``` ## Permissions Access control at the attribute level is not yet supported, but will be in a future release. By default, there are no access controls on the database and they must be configured by adding a `permissions` definition to the schema. Each collection in a schema can have a `permissions` object that defines the access control rules for that collection. Once a permissions object is defined, Triplit will enforce the provided rules for each operation on the collection. If no rules for an operation are provided, the operation not be allowed by default. The following example turns off all access to the `todos` collection so it is only accessible with your [`service` token](/auth#tokens): ```typescript filename="schema.ts" copy const roles: Roles = { // Role definitions }; const schema = S.Collections({ todos: { schema: S.Schema({ id: S.Id(), text: S.String(), authorId: S.String(), }), permissions: , }, }); ``` Collection permissions are defined for each operation and role. If a role is not included, it will not be allowed to perform that operation. When performing each operation, Triplit will check the set of set of [filter clauses](/query/where) that must be satisfied for the operation to be allowed. ```json { "role": { "operation": { "filter": // Boolean filter expression } } } ``` ### Read To allow clients to read data, you must define a `read` permission that specifies the roles that may read data and any additional restrictions. The following example allows a `user` to read the todos that they authored and an `admin` to read any todo: ```typescript filename="schema.ts" copy const roles: Roles = { // Role definitions }; const schema = S.Collections({ todos: { schema: S.Schema({ id: S.Id(), text: S.String(), authorId: S.String(), }), permissions: { admin: { read: { // Allow all reads filter: [true], }, }, user: { read: { // Allow reads where authorId is the user's id filter: [['authorId', '=', '$role.userId']], }, }, }, }, }); ``` ### Insert To allow clients to insert data, you must define an `insert` permission that specifies the roles that may insert data and any additional restrictions. The following example allows a `user` to insert a todo that they author and an `admin` to insert any todo: ```typescript filename="schema.ts" copy const roles: Roles = { // Custom role definitions }; const schema = S.Collections({ todos: { schema: S.Schema({ id: S.Id(), text: S.String(), authorId: S.String(), }), permissions: { admin: { insert: { // Allow all inserts filter: [true], }, }, user: { insert: { // Allow inserts where authorId is the user's id filter: [['authorId', '=', '$role.userId']], }, }, }, }, }); ``` ### Update To allow users to update data, you must define an `update` permission that specifies the roles that may update data and any additional restrictions. For updates, the permission is checked against the "old" state of the entity, before it has been updated. The following example allows a `user` to update todos that they authored and an `admin` to update any todo: ```typescript filename="schema.ts" copy const roles = { // Custom role definitions }; const schema = S.Collections({ todos: { schema: S.Schema({ id: S.Id(), text: S.String(), authorId: S.String(), }), permissions: { admin: { update: { // Allow all updates filter: [true], }, }, user: { update: { // Allow updates where authorId is the user's id filter: [['authorId', '=', '$role.userId']], }, }, }, }, }); ``` ### Post update You may also optionally define a `postUpdate` permission that will be run after an update operation has been completed. This is useful for confirming that updated data is valid. For example, this checks that a `user` has not re-assigned a todo to another `user`: ```typescript filename="schema.ts" copy const roles: Roles = { // Custom role definitions }; const schema = S.Collections({ todos: { schema: S.Schema({ id: S.Id(), text: S.String(), authorId: S.String(), }), permissions: { user: { update: { // Allow updates where authorId is the user's id filter: [['authorId', '=', '$role.userId']], }, postUpdate: { // Check that the authorId has not changed filter: [['authorId', '=', '$role.userId']], }, }, }, }, }); ``` ### Delete To allow users to delete data, you must define a `delete` permission that specifies the roles that may delete data and any additional restrictions. The following example allows a `user` to delete todos that they authored and an `admin` to delete any todo: ```typescript // schema.ts const roles: Roles = { // Custom role definitions }; const schema = S.Collections({ todos: { schema: S.Schema({ id: S.Id(), text: S.String(), authorId: S.String(), }), permissions: { admin: { delete: { // Allow all deletes filter: [true], }, }, user: { delete: { // Allow deletes where authorId is the user's id filter: [['authorId', '=', '$role.userId']], }, }, }, }, }); ``` ## Editing permissions Permissions are a part of your schema and can be added or updated by [modifying your schema](/schemas/updating). In a future release, you will be able to manage permissions in your project's [Dashboard](https://www.triplit.dev/dashboard). ## Modeling permissions with external authentication When using an external authentication provider like [Clerk](/auth/integration-guides/clerk), the provider is the source of truth for identifying users. This means that in your Triplit database you might not need a traditional users collection. Permissions that restrict access to specific authenticated users should use the ids provided by the auth service. If you want to store additional information about a user in Triplit, we recommend using a `profiles` collection that uses the same ID as the user ID provided from your auth provider. When your app loads and a user authenticates, you can fetch their profile or create it if it doesn't exist. Here’s an example schema: ```typescript filename="schema.ts" copy const roles: Roles = { user: { match: { sub: '$userId', }, }, }; const schema = S.Collections({ profiles: { schema: S.Schema({ id: S.Id(), // Use the user ID from your auth provider when inserting nickname: S.String(), created_at: S.Date(), }), permissions: { user: { read: { filter: [['id', '=', '$role.userId']], }, update: { filter: [['id', '=', '$role.userId']], }, insert: { filter: [['id', '=', '$role.userId']], }, }, }, }, todos: { schema: S.Schema({ id: S.Id(), text: S.String(), authorId: S.String(), }), permissions: { user: { read: { filter: [['authorId', '=', '$role.userId']], }, insert: { filter: [['authorId', '=', '$role.userId']], }, update: { filter: [['authorId', '=', '$role.userId']], }, delete: { filter: [['authorId', '=', '$role.userId']], }, }, }, }, }); ``` ## Modeling selective public access Sometimes you may want to allow a user to share a link to a resource that is not publicly accessible. For example, you have a table `documents` and only the author can read their own documents. ```typescript const schema = S.Collections({ documents: { schema: S.Schema({ id: S.Id(), title: S.String(), content: S.String(), authorId: S.String(), }), permissions: { authenticated: { read: { filter: [ // Only the author can read their own documents ['authorId', '=', '$role.userId'], ], }, }, }, }, }); ``` To allow selective public access, you can use the `or` function to add another filter with a [`$query` variable](/query/variables#query-variables), allowing the requesting user to read the document if they know the id. ```typescript const schema = S.Collections({ documents: { schema: S.Schema({ id: S.Id(), title: S.String(), content: S.String(), authorId: S.String(), }), permissions: { authenticated: { read: { filter: [ or([ // Only the author can read their own documents ['authorId', '=', '$role.userId'], // Anyone can read the document if they know the id ['id', '=', '$query.docId'], ]), ], }, }, }, }, }); ``` A client requesting the document can use the `Vars` method on the query builder to pass in the `docId` variable to the query: ```typescript const query = client .query('documents') .Vars() // Allows access to the document with id 1234 .Where('id', '=', '1234'); // Filters to just the document with id 1234 const document = await client.fetch(query); ``` Now you can implement a shareable link like `https://myapp.com/share/1234` and use that id (`1234`) to fetch a document as needed! --- ## schemas/relations # Relations To define a relationship between two collections, you define a subquery that describes the relationship with `RelationMany`, `RelationOne` or `RelationById`. while `RelationOne` and `RelationById` are designed for singleton relations and will be directly nested or a sub-object or `null` if an applicable entity doesn't exist. Within a relation, either in a where clause or the `RelationById` id, parameter, you can reference the current collection's attributes with `$`. ## RelationMany A `RelationMany` attribute will be in the shape `Array<Entity>`. It's designed to model a one-to-many relationship between two collections. If no related entities are found, the attribute will be an empty array. In this example schema, we are modeling a school, where departments have many classes. The `departments` collection has a `classes` attribute that is a `RelationMany` to the `classes` collection. ```typescript const schema = S.Collections({ departments: { schema: S.Schema({ id: S.Id(), name: S.String(), }), relationships: { classes: S.RelationMany('classes', { where: [['department_id', '=', '$id']], }), }, }, classes: { schema: S.Schema({ id: S.Id(), name: S.String(), level: S.Number(), building: S.String(), department_id: S.String(), }), }, }); ``` ## RelationOne A `RelationOne` attribute will be an `Entity` or `null`. It's designed to model a one-to-one relationship between two collections. The `RelationOne` attribute will be the related entity or `null` if no related entity is found. We can update our model of a school, so that a class has a relation to its department. ```typescript const schema = S.Collections({ departments: { schema: S.Schema({ id: S.Id(), name: S.String(), }), relationships: { classes: S.RelationMany('classes', { where: [['department_id', '=', '$id']], }), }, }, classes: { schema: S.Schema({ id: S.Id(), name: S.String(), level: S.Number(), building: S.String(), department_id: S.String(), }), relationships: { department: S.RelationOne('departments', { where: [['id', '=', '$department_id']], }), }, }, }); ``` ## RelationById RelationById is a special case of `RelationOne` that is used to define a relationship by a foreign key. The `RelationById` attribute will be the related entity or `null` if no related entity is found. We can update the previous example to use `RelationById` instead of `RelationOne`. ```typescript const schema = S.Collections({ departments: { schema: S.Schema({ id: S.Id(), name: S.String(), }), relationships: { classes: S.RelationMany('classes', { where: [['department_id', '=', '$id']], }), }, }, classes: { schema: S.Schema({ id: S.Id(), name: S.String(), level: S.Number(), building: S.String(), department_id: S.String(), }), relationships: { department: S.RelationById('departments', '$department_id'), }, }, }); ``` ## Querying collections with relations By default, queries on collections with relations will _not_ return related data. You can use the `include` method to specify which relations you want to include in the query. ```typescript const classesQuery = client.query('classes').Include('department'); const departmentsQuery = client.query('departments').Include('classes'); ``` ## Defining relations with referential variables TODO --- ## schemas/types # Data types When using a schema you have a few datatypes at your disposal: ### Collections and Schema types The `Collections` and `Schema` schema types are used to define your collections and their attributes. They are simple record types but will help provide type hinting and validation to their parameters. ```typescript const schema = S.Collections({ todos: { schema: S.Schema({ id: S.Id(), // Additional attributes here... }), }, }); ``` ### Primitive types Primitive types are basic types for the database. #### String The string data type is used to store text. ```typescript const stringType = S.String({ // options }); ``` Valid options for the string type include: - `nullable`: Indicates the value is optional when used in a `Record`. This is equivalent to wrapping the attribute in [`S.Optional`](#optional-keys). - `default`: Provides a default value or function for the attribute. Possible values include: - `S.Default.uuid()`: Generates a random UUID. - `string`: A literal string value. - `enum`: An array of strings that restricts the possible values of the attribute. This will perform runtime validation and provide autocomplete in your editor. For information about operators that can be used with strings in `where` statements, see the [Where clause documentation](/query/where#operators). #### Number The number data type is used to store integer or float numbers. ```typescript const numberType = S.Number({ // options }); ``` Valid options for the number type include: - `nullable`: Indicates the value is optional when used in a `Record`. This is equivalent to wrapping the attribute in [`S.Optional`](#optional-keys). - `default`: Provides a default value or function for the attribute. Possible values include: - `number`: A literal number value. For information about operators that can be used with numbers in `where` statements, see the [Where clause documentation](/query/where#operators). #### Boolean The boolean data type is used to store true or false values. ```typescript const booleanType = S.Boolean({ // options }); ``` Valid options for the boolean type include: - `nullable`: Indicates the value is optional when used in a `Record`. This is equivalent to wrapping the attribute in [`S.Optional`](#optional-keys). - `default`: Provides a default value or function for the attribute. Possible values include: - `boolean`: A literal boolean value. For information about operators that can be used with booleans in `where` statements, see the [Where clause documentation](/query/where#operators). #### Date The date data type is used to store date and time values. ```typescript const dateType = S.Date({ // options }); ``` Valid options for the date type include: - `nullable`: Indicates the value is optional when used in a `Record`. This is equivalent to wrapping the attribute in [`S.Optional`](#optional-keys). - `default`: Provides a default value or function for the attribute. Possible values include: - `S.Default.now()`: Generates the current date and time. - `string`: An ISO 8601 formatted string. For information about operators that can be used with dates in `where` statements, see the [Where clause documentation](/query/where#operators). ### Set Set types are used to store a collection of non nullable value types. Sets are unordered and do not allow duplicate values. Lists, which support ordering and duplicate values, are on the [roadmap](https://triplit.dev/roadmap). ```typescript const stringSet = S.Set(S.String(), { // options }); ``` The first argument to the `Set` constructor is the type of the values in the set. This can be any of the primitive types, including `S.String()`, `S.Number()`, `S.Boolean()`, or `S.Date()`. Valid options for the set type include: - `nullable`: Indicates the value is optional when used in a `Record`. This is equivalent to wrapping the attribute in [`S.Optional`](#optional-keys). - `default`: Provides a default value or function for the attribute. Possible values include: - `S.Default.Set.empty()`: Generates an empty set. For information about operators that can be used with sets in `where` statements, see the [Where clause documentation](/query/where#operators). ### Record The record types allow you model nested information with known keys, similar to a struct in C. ```typescript const recordType = S.Record( { street: S.String(), city: S.String(), state: S.String(), zip: S.String(), }, { // options } ); ``` The first argument to the `Record` constructor is an object that defines the keys and their types. This can be any data type. Valid options for the record type include: - `nullable`: Indicates the value is optional when used in a `Record`. This is equivalent to wrapping the attribute in [`S.Optional`](#optional-keys). For information about operators that can be used with records in `where` statements, see the [Where clause documentation](/query/where#operators). #### Optional keys You can indicate an attribute is optional by passing the `` option to its constructor or wrapping the attribute in `S.Optional`. Optional attributes may not exist, have the value `undefined`, or have the value `null` - these are all equivalent in Triplit. Under the hood `S.Schema()` is a record type, so optional attributes allow you to define optional keys in your schema as well. ```typescript const schema = S.Collections({ test: { schema: S.Schema({ id: S.Id(), // S.Optional and nullable are equivalent optionalString: S.Optional(S.String()), alsoOptionalString: S.String(), }), }, }); await client.insert('test', { id: '123', }); // await client.update('test', '123', (e) => { e.optionalString = 'hello'; }); // await client.update('test', '123', (e) => { delete e.optionalString; }); // ``` For information about operators that can be used with optional attributes in `where` statements, see the [Where clause documentation](/query/where#operators). ### Json The json type is used to store arbitrary JSON data that is [spec](https://datatracker.ietf.org/doc/html/rfc7159) compliant. This type is useful for storing unstructured data or data that may change frequently. Valid primitive types for the json type include: - `string` - `number` - `boolean` - `null` You may also store arrays and objects containing any of the above types. ```typescript const jsonType = S.Json({ // options }); ``` Valid options for the json type include: - `nullable`: Indicates the value is optional when used in a `Record`. This is equivalent to wrapping the attribute in [`S.Optional`](#optional-keys). - `default`: Provides a default value or function for the attribute. Possible values include: - `json`: A literal JSON value. - Any default value for the primitive types that are JSON compliant. --- ## schemas/updating # Safely updating schemas in production ## Client compatibility When a client connects to a Triplit server, it compares the schema it has stored locally with the schema on the server. If the schemas are incompatible, the client will refuse to connect to the server. This is a safety feature to prevent data corruption. That does not mean that you can't update your schema on the server, but you must do so in a way that is backwards compatible. This page describes the tools Triplit provides to make this process as smooth as possible. ## Getting setup Let start with a simple schema, defined at `./triplit/schema.ts` in your project directory. ```typescript filename="./triplit/schema.ts" copy const schema = S.Collections({ todos: { schema: S.Schema({ id: S.Id(), text: S.String(), completed: S.Boolean(), }), }, }); ``` You can start the development server with the schema pre-loaded. ```bash copy triplit dev ``` By default, the server will use in-memory storage, meaning that if you shut down the server, all of your data will be lost. This can be useful when you're early in development and frequently iterating on your schema. If you like this quick feedback loop but don't want to repeatedly re-insert test data by hand, you can use Triplit's [`seed` commands](/seeding). You can use seed command on its own: ```bash copy triplit seed run my-seed ``` Or use the `--seed` flag with `triplit dev`: ```bash copy triplit dev --seed=my-seed ``` If you want a development environment that's more constrained and closer to production, consider using the [SQLite](https://www.sqlite.org/) persistent storage option for the development server: ```bash copy triplit dev --storage=sqlite ``` Your database will be saved to `triplit/.data`. You can delete this folder to clear your database. ## Updating your schema Let's assume you've run some version of `triplit dev` shown above and have a server up and running with a schema. You've also [properly configured your `.env`](/getting-started#syncing-in-local-development) such that Triplit CLI commands will be pointing at it. Let's also assume you've added some initial todos: ```ts copy filename="App.tsx" const client = new TriplitClient({ schema, serverUrl: import.meta.env.VITE_TRIPLIT_SERVER_URL, token: import.meta.env.VITE_TRIPLIT_TOKEN, }); client.insert('todos', ); client.insert('todos', ); client.insert('todos', ); ``` ### Adding an attribute Now let's edit our schema by adding a new `tagId` attribute to `todos`, in anticipation of letting users group their todos by tag. ```typescript filename="./triplit/schema.ts" copy const schema = S.Collections({ todos: { schema: S.Schema({ id: S.Id(), text: S.String(), completed: S.Boolean(), tagId: S.String(), }), }, }); ``` ### Pushing the schema We're trying to mimic production patterns as much as possible, so we're not going to restart the server to apply this change (and in fact, that would cause problems, as we'll soon see). Instead let's use a new command: ```bash triplit schema push ``` This will look at the schema defined at `./triplit/schema.ts` and attempt to apply it to the server while it's still running. In our case, it fails, and we get an error like this: ``` ✖ Failed to push schema to server Found 1 backwards incompatible schema changes. Schema update failed. Please resolve the following issues: Collection: 'todos' 'tagIds' Issue: added an attribute where optional is not set Fix: make 'tagIds' optional or delete all entities in 'todos' to allow this edit ``` What's at issue here is that we **tried to change the shape/schema of a todo to one that no longer matches those in the database**. All attributes in Triplit are required by default, and by adding a new attribute without updating the existing todos, we would be violating the contract between the schema and the data. Thankfully, the error gives us some instructions. We can either 1. Make `tagId` optional e.g. `tagIds: S.Optional(S.String())` and permit existing todos to have a `tagId` that's `undefined`. 2. Delete all of the todos in the collection so that there isn't any conflicting data. While 2. might be acceptable in development, 1. is the obvious choice in production. In production, we would first add the attribute as optional, backfill it for existing entities with calls to `client.update`, as well as upgrade any clients to start creating new todos with `tagId` defined. Only when you're confident that all clients have been updated to handle the new schema and all existing data has been updated to reflect the target schema, should you proceed with a backwards incompatible change. Whenever you try to `triplit schema push`, the receiving database will run a diff between the current schema and the one attempting to be applied and surface issues like these. Here are all possible conflicts that may arise. ### Fixing the issues Let's make `tagId` optional: ```typescript filename="./triplit/schema.ts" copy const schema = S.Collections({ todos: { schema: S.Schema({ id: S.Id(), text: S.String(), completed: S.Boolean(), tagId: S.Optional(S.String()), }), }, }); ``` Now we can run `triplit schema push` again, and it should succeed. For completeness, let's also backfill the `tagId` for existing todos: ```ts copy await client.transact(async (tx) => { const allTodos = await tx.fetch(client.query('todos')); for (const [_id, todo] of allTodos) { await tx.update('todos', todo.id, (entity) => { entity.tagId = 'chores'; }); } }); ``` We've now successfully updated our schema in a backwards compatible way. If you're confident that all clients have been updated to handle the new schema and all existing data has been updated to reflect the target schema, you can then choose to make `tagId` required. ## Handling backwards incompatible changes ### Adding an attribute where optional is not set Like in the example above, these changes will be backwards incompatible if you have existing entities in that collection. In production, only add _optional_ attributes, and backfill that attribute for existing entities. ### Removing a non-optional attribute This is a backwards incompatible change, as it would leave existing entities in the collection with a missing attribute. In production, deprecate the attribute by making it optional, delete the attribute from all existing entities (set it to `undefined`), and then you be allowed to remove it from the schema. ### Removing an optional attribute While not technically a backwards incompatible change, it would lead to data loss. In production, delete the attribute from all existing entities first (set it to `undefined`) and then it will be possible to remove it from the schema. ### Changing an attribute from optional to required This is a backwards incompatible change, as existing entities with this attribute set to `undefined` will violate the schema. In production, update all existing entities to have a non-null value for the attribute, and then you will be able to make it required. ### Changing the type of an attribute Triplit will prevent you from changing the type of an attribute if there are existing entities in the collection. In production, create a new optional attribute with the desired type, backfill it for existing entities, and then remove the old attribute following the procedure described above ("Removing an optional attribute"). ### Changing the type of a set's items This is similar to changing the type of an attribute, but for sets. In production, create a new optional set attribute with the desired type, backfill it for existing entities, and then remove the old set following the procedure described above ("Removing an optional attribute"). ### Changing an attribute from nullable to non-nullable Triplit will prevent you from changing an attribute from nullable to non-nullable if there are existing entities in the collection for which the attribute is `null`. In production, update all of the existing entities to have a non-null value for the attribute and take care that no clients will continue writing `null` values to the attribute. Then you will be able to make it non-nullable. ### Changing a string to an enum string or updating an enum Triplit will prevent you from changing a string to an enum string or updating an enum if there are existing entities in the collection with values that are not in the new enum. In production, update all of the existing entities to have a value that is in the new enum and then you will be able to make the change. ### Removing a relation Because relations in Triplit are just views on data in other collections, removing a relation will not corrupt data but can still lead to backward-incompatible behavior between client and server. For instance, if the server's schema is updated to remove a relation, but an out-of-date client continues to issues queries with clauses that reference that relation, such as `include`, a relational `where` filter, or an `exists` filter, the server will reject those queries. In production, you may need to deprecate clients that are still using the old relation and force them to update the app with the new schema bundled in. --- # Triplit Cloud ## triplit-cloud/index # Triplit Cloud Triplit Cloud is a service for managing your Triplit projects. It's free for individuals to use, and integrates with your Triplit server, whether it's managed by us or self-hosted. When you integrate with Triplit Cloud you get access to a range of features, including: - **The Triplit Console**: A graphical interface for managing your data and schema. - **Authorization**: Configure access to your server without needing to modify your server's code. To get started with Triplit Cloud, read one of our guides below: - [Deploying a Triplit-hosted server](/triplit-cloud/managed-machines) - [Deploying a self-hosted server](/triplit-cloud/self-hosted-deployments) --- ## triplit-cloud/managed-machines # Managed machines Triplit Cloud offers managed machines for hosting your Triplit server. Managed machines are a great way to get started with Triplit, as they require no setup or maintenance on your part. We handle all the server management, so you can focus on building your app. ### Create a project - [Sign-in or sign-up for a Triplit account](https://triplit.dev/dashboard) - Click the **Create Project** button - Provide a name for your project. This will act as a friendly alias for your project throughout the dashboard. ### Choose and deploy a machine Once you've created a project, you'll be presented with a choice of deployment options under the **Triplit Cloud** tab: Once you've chosen a deployment machine configuration and region, click the **Deploy** button. This will bring you to a checkout page, where you can review your order and enter your payment details. ### View your machine Once your machine is deployed, you can view its status on the **Overview** tab of your project dashboard. Here you'll see information about your machine, including its region, storage and status. ### Use the dashboard You can use the Triplit dashboard to: - Mint API keys and rotate them as needed - Allow your server to accept external tokens signed with different secrets - Manage your data and schema with the [Triplit Console](/triplit-cloud/triplit-console) Copy the secrets from the **Overview** tab (pictured below) into your app's `.env` file.  --- ## triplit-cloud/self-hosted-deployments # Self-hosted deployments Triplit Cloud is fully compatible with self-hosted Triplit servers. This means you can deploy your own Triplit server and connect it to the Triplit Cloud dashboard, and reap the benefits of easier server management and use of the Triplit Console. ### Create a project - [Sign-in or sign-up for a Triplit account](https://triplit.dev/dashboard) - Click the **Create Project** button - Provide a name for your project. This will act as a friendly alias for your project throughout the dashboard. ### Deploy your project After creating your project, you'll be shown some information about your project, and instructions for deploying a self-hosted machine. You have two options for deploying a self-hosted machine: - **Railway**: One-click-deploy a Triplit server using Railway. This is the easiest way to get started with a self-hosted machine, and won't require any configuration. - **Docker**: Deploy a Triplit server using Docker on a platform of your choice. This will require you to configure your server with the necessary environment variables. We plan on adding many more one-click-deploy options in the future, so stay tuned! #### Configure your server (Docker only) If you're not using one of the one-click deploy options, you'll need to configure your server to accept requests from the Triplit Dashboard. <Callout type="warning" emoji="⚠️"> If you plan to connect your self-hosted Triplit server to the Triplit Dashboard and use JWTs for authentication and permission within Triplit, EXTERNAL_JWT_SECRET should only be set on your Triplit Dashboard. Ensure the env var is NOT included wherever you deployed your Docker image. Otherwise, you may encounter errors related to invalid JWTs and JWT signatures. </Callout> ##### `JWT_SECRET` The secret used to sign and verify JWT tokens. To use the Triplit Dashboard, set this to the official Triplit public key, provided below. ```text copy -----BEGIN PUBLIC KEY-----MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA3/h/YykqVaiyqxDpKqyafdsIiGOi/xZ5CXkwzlG8EHfs9L6eB+/zYQ3Aiqvb2ysTCqS53aD2Ktiv44s9Xs1yHNpZkAIQugKdeERhZAQm20DA3DHj1ONZ9jCduUV5C99y5uM06+6FdfwYjrVWpPjuKwdzU+/f5Q4rsG3K2vSRFQX7UYJhnqSfeqXZZ0n5WGqDzRVsSLDgpBtIba7cGQ8v6NJgdRk95SksOXJ/srnSeogWZ2+V6X6J/fxcYwRQYa+YFBcY8ReOWr79pdAScGF0fA89GctCwdxcpVlGHP9zbQe6wY5/LHm84iQ4WP8w8azpNcm33DX3QbBbY8c3YPzEyt3qcOTrDqwNPEwsARmf3p2SmkVntB7T89Ca33ppRTPKA6BknbESgE6ShKOoaHC15ZexJP7AYNJ5ap8eXFhlKEM9VfytfkshNgmq7SC0M9WKcrtQFAvpoh0ggzIC0yh/A8ndUCo3DA58p5aRjYOgliuzeQCkI6kRK8fnJKX38q91mhma7lf4nYkYxbhorSTRqS3VLyHSTx9AbiWLYl8zdqAWGYVuysXdKKY1kpQzzwqcY5RT/CEDsVIkO2GB4iNzMqkA3hI3673oBEE+/PhkeIqE2WmL15SCR65OIj7L0XBZgVhqpPdICctD5Xktc684kpdqT2jKW0h2O53iZXFiWhMCAwEAAQ==-----END PUBLIC KEY----- ``` ##### `LOCAL_DATABASE_URL` (required for durable storage) An absolute path on the server's file system to a directory where the server will store any database files. This is required for durable storage options: `lmdb`, and `sqlite`. ##### `CLAIMS_PATH` (optional) If you are using custom JWTs with nested Triplit-related claims, you can set the `CLAIMS_PATH` environment variable. The server will read the claims at the path specified by `CLAIMS_PATH`. Read the [authentication guide](/auth) for more information. ##### `SENTRY_DSN` (optional) If you want to log errors to Sentry, you can set the `SENTRY_DSN` environment variable. The server will automatically log errors to Sentry. ##### `VERBOSE_LOGS` (optional) If you want to log all incoming and outgoing messages and requests, you can set the `VERBOSE_LOGS` environment variable. This can be useful for debugging. #### `MAX_BODY_SIZE` (optional) If you want to increase the maximum body size for incoming requests, you can set the `MAX_BODY_SIZE` environment variable. This is useful if you want to send large payloads to your server. The default value is 100, corresponding to 100MB. ### Add your server's hostname to your project Once you have a Triplit server running and [properly configured](#configuration), enter its hostname into the form and click **Connect**. ### Use the dashboard You can use the Triplit dashboard to: - Mint API keys and rotate them as needed - Allow your server to accept external tokens signed with different secrets without modifying your server's code - Manage your data and schema with the [Triplit Console](/triplit-cloud/triplit-console) To enable these benefits, your app will connect to a `https://<project-id>.triplit.io` URL. This will route your requests to your self-hosted server. Copy the secrets from the **Overview** tab (pictured below) into your app's `.env` file.  --- ## triplit-cloud/triplit-console # Triplit Console The Triplit Console is a graphical interface for managing your data and schema. It's available for Triplit Cloud projects, whether they're connected to a managed machine or a self-hosted one. ## Features The Triplit Console offers a range of features to help you manage your data and schema: ### Manage your data You can insert entities into your collections. And update them inline. ---