Getting Started
Build your first LinkedRecords application
By the end of this tutorial, you will have a working application with authentication enabled, authorization out of the box, the ability to share data between users, and persistent storage - all without writing any backend code.
Quick Start
Start the LinkedRecords backend locally with a single command (requires Docker):
This docker-compose.yml is for local development only. It uses a built-in mock OIDC provider with one-click test logins and is not suitable for production use.
This starts LinkedRecords on port 6543. Once you see LinkedRecords is running on port 6543, the backend is ready.
Building Your First App
This section guides you through implementing a small "Hello World" single-page application to get to know the main LinkedRecords features.
The example uses Vite and React, but LinkedRecords is not limited to these tools.
Initialize a new Project
Setup React Using Vite
Vite is a modern TypeScript/JavaScript build system which bundles our code and prepares it for deployment. During development it updates the app in the browser as we change the code in our IDE.
We can use npm to create a Vite + TypeScript + React scaffold by running the following
commands in our terminal:
Clean up Scaffold App
Next, we clean up the scaffold app a little to have a greenfield to start from.
Delete the following files and directory:
And replace the content of src/App.tsx with the following:
Install NPM Packages
To use LinkedRecords in our React single page application, we need to install the npm package:
You can also use LinkedRecords outside of React applications. The linkedrecords-react module provides some handy hooks which make our lives easier.
Add Tailwind CSS
To make our app look polished, we'll add Tailwind CSS:
Update vite.config.ts to include the Tailwind plugin:
Replace the content of src/index.css with:
Start the Development Server
Now that all dependencies and configuration are in place, start the Vite development server:
If you already started the dev server earlier, restart it now (Ctrl+C and run npm run dev again) to pick up the Tailwind and Vite configuration changes.
You will see a URL in your terminal (e.g. http://localhost:5173/). Keep the server running—Vite will automatically reload the app as you make changes in the following sections.
Implement a Simple todo App
Next we are going to implement a simple todo list application using LinkedRecords.
To make LinkedRecords available in our app we need to wrap it into the LinkedRecords
provider. Replace the content of src/main.tsx with:
The LinkedRecords provider expects a URL of the LinkedRecords backend as property (serverUrl). In this case it is a
LinkedRecords setup which runs locally.
For the actual app we replace the content of src/App.tsx with the following:
With the dev server still running, open the URL from your terminal (e.g. http://localhost:5173/) in your browser. You'll notice that with very little code:
- We will be prompted to login. The LinkedRecords backend takes care of user management.
- If we reload the page, all todos are persisted.
- If we log in as another user, we see different todos. The todos are scoped to a user.
In the next sections we will extend the app to learn about a few other LinkedRecords features. Especially how multiple users can collaborate on the same todos.
Display the Logged-in User
Most applications display who is currently logged in. This helps users confirm they're using the correct account and provides a familiar navigation element.
LinkedRecords provides a method to retrieve the current user's email address. Let's add a UserInfo component that displays this information.
Add this new component above the App component:
Then update the App component to include it (highlighted line is new):
Now when you open the app, you'll see the email address of the currently logged-in user displayed at the top. If you open the app in a different browser and log in as another user, you can easily tell which account you're using in each window.
Add an "Archive" feature
This section adds a button next to each todo which allows to archive a todo. Archived todos will then be listed in a second list and can be unarchived again.
Declare the New Terms First
Before we can use 'Archived' and 'Active' as values in our facts, we need to declare them as terms. This is a key LinkedRecords concept: any term you reference in a fact must be declared first using $isATermFor.
Update the lr.Fact.createAll call in the useEffect hook of the App component (highlighted lines are new):
Why do we need to declare terms?
When you create a fact like [todoId, 'stateIs', 'Archived'], LinkedRecords checks permissions:
- You need
$canRefinepermission on the subject (todoId) - You need
$canReferTopermission on the object ('Archived')
Terms are special: once declared with $isATermFor, any user can refer to them as an object in facts—but no one can use a term as a subject. This makes terms ideal for shared vocabulary like state names, types, and categories.
If you skip this step, archiving will silently fail because 'Archived' won't be a recognized term that users can refer to.
Update the TodoList Component
Now replace the TodoList component with the following code (highlighted lines are new):
Add a "Share" feature
Next, we are going to implement a share button. Once a user clicks that button, they will be prompted to provide an email address of the person with whom they want to share the todo.
The user you want to share with must have signed up first. If you're using the dev setup with Docker Compose, open the app in an incognito window or a different browser, go through the login flow, and select one of the test accounts (alice@example.com, bob@example.com, or charlie@example.com) or enter a custom email address. Only after completing the login flow can you invite that user.
To implement the feature, add the shareTodo function and Share button to the TodoList component (highlighted lines are new):
Understanding Permission Limitations
If you test the share functionality now, you'll notice something interesting: the user you shared the todo with can see it and even check/uncheck the completed checkbox, but they cannot archive or unarchive the todo. Try it—the archive button will silently fail for them.
Why does this happen? This is because of how LinkedRecords handles different types of permissions:
$canAccessgrants access to read and modify the attribute's content (the JSON data liketitleandcompleted)- However, archiving requires creating a fact like
[todoId, 'stateIs', 'Archived']- this uses the todo as the subject of a fact
To create facts where an entity is the subject, a user needs the $canRefine permission. Think of it as the difference between:
- Editing what's inside a document (
$canAccess) - Adding metadata about the document (
$canRefine)
LinkedRecords Permission Model:
$canAccess— Read and write the attribute's value$canRead— Read-only access to the attribute's value$canRefine— Create facts where this entity is the subject (e.g.,[entity, 'stateIs', 'Archived'])$canReferTo— Create facts where this entity is the object (e.g.,[something, 'belongsTo', entity])
To fix this, update the shareTodo function to also grant $canRefine:
Now the shared user can archive and unarchive todos too. This pattern of combining permissions is common in LinkedRecords—you grant exactly the capabilities each user needs, no more, no less.
Already shared todos won't be fixed automatically!
Updating your code only affects future shares. Todos you've already shared are still missing the $canRefine permission because that fact was never created for them.
To fix an existing share, the user would need to share it again — this time the updated code will create both permission facts.
This is an important concept: permissions in LinkedRecords are stored as facts in the database, not derived from code. Changing your code changes what happens next time, but doesn't retroactively update existing data.