apps/docs/content/sdk-features/attribution.mdx
The attribution system lets you track which users create or edit shapes. Connect tldraw to your auth system through a TLUserStore to resolve display names, render attribution labels, and persist user records alongside your document data. The built-in note shape uses attribution to show who last edited a note's text, and you can add similar tracking to custom shapes.
A TLUserStore provides a reactive currentUser signal for the active user and an optional resolve method for looking up other users by ID. Pass it as the users prop on the Tldraw component or the useSync hook:
import { computed, createUserId, Tldraw, TLUserStore, UserRecordType } from 'tldraw'
import 'tldraw/tldraw.css'
const currentUser = computed('currentUser', () =>
UserRecordType.create({
id: createUserId('user-123'),
name: 'Alice',
color: '#e03131',
})
)
const users: TLUserStore = {
currentUser,
}
export default function App() {
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw users={users} />
</div>
)
}
When no users prop is provided, the editor derives the current user from user preferences. With useSync, other users are resolved from collaborator presence.
The optional resolve method looks up users by their raw ID string. The editor calls it first whenever it needs a display name or user record for an ID:
import {
computed,
createCachedUserResolve,
createUserId,
TLUserStore,
UserRecordType,
} from 'tldraw'
const users: TLUserStore = {
currentUser: computed('currentUser', () =>
UserRecordType.create({
id: createUserId('user-123'),
name: 'Alice',
color: '#e03131',
})
),
resolve: createCachedUserResolve((userId) => {
// Look up the user from your auth system
return myUserCache.get(userId) ?? null
}),
}
The createCachedUserResolve helper wraps a lookup function so that each user ID gets a single stable reactive signal. If you write your own resolve, return the same signal for repeated calls with the same ID.
Attribution is opt-in per shape type. Each shape util decides what to track and when to stamp a user ID. When you stamp a user ID with Editor#getAttributionUserId, the editor also writes a corresponding user: record to the store so that display names survive across sessions, clipboard paste, and .tldr file exports.
The built-in note shape tracks who last edited its text. Whenever a note's rich text changes and is non-empty, NoteShapeUtil sets the textLastEditedBy prop to the current user's ID via editor.getAttributionUserId(). Clearing the text resets it to null, and duplicating or pasting a note with text re-stamps the copy to the current user. The note renders the last editor's first name as a small label in the corner.
The Editor provides three methods for working with attribution. Editor#getAttributionUserId returns the current user's raw ID string (without the user: prefix). Editor#getAttributionDisplayName and Editor#getAttributionUser check the TLUserStore first, then fall back to user: records in the store:
import { useEditor, useValue } from 'tldraw'
function AttributionLabel({ userId }: { userId: string }) {
const editor = useEditor()
const name = useValue('attribution-name', () => editor.getAttributionDisplayName(userId), [
editor,
userId,
])
if (!name) return null
return <span>{name}</span>
}
Add attribution tracking to your own shapes by storing a user ID in your shape's props and overriding ShapeUtil#getReferencedUserIds:
import { ShapeUtil, T, TLBaseShape } from 'tldraw'
type MyShape = TLBaseShape<
'my-shape',
{
createdBy: string | null
}
>
class MyShapeUtil extends ShapeUtil<MyShape> {
static override type = 'my-shape' as const
static override props = {
createdBy: T.string.nullable(),
}
override getReferencedUserIds(shape: MyShape) {
return shape.props.createdBy ? [shape.props.createdBy] : []
}
// ... other ShapeUtil methods
}
When shapes are copied to the clipboard or exported, the editor includes the user: records returned by getReferencedUserIds. Display names then remain available on the other side.
To stamp the current user when creating shapes:
const userId = editor.getAttributionUserId()
editor.createShape({
type: 'my-shape',
props: {
createdBy: userId,
},
})
Extend user records with custom metadata by passing validators to createTLSchema:
import { createTLSchema, T } from 'tldraw'
const schema = createTLSchema({
user: {
meta: {
department: T.string,
isAdmin: T.boolean,
},
},
})
Custom metadata is validated and persisted alongside the standard user fields. Access it through the meta property on TLUser records.
| Symbol | Description |
|---|---|
| Editor#getAttributionUserId | Get the current user's ID for stamping shapes. Returns string | null |
| Editor#getAttributionDisplayName | Resolve a display name from a user ID. Returns string | null |
| Editor#getAttributionUser | Resolve a full TLUser record from a user ID |
| TLUserStore | Interface for connecting to your auth/user system |
| TLUser | A user record in the store |
| UserRecordType | The default user record type |
| createUserId | Create a typed user ID |
| createCachedUserResolve | Create a cached resolve function for TLUserStore |
| createUserRecordType | Build a user record type with custom meta validators |
For setting up user identity in multiplayer, see the Collaboration page.