--- title: Attribution created_at: 3/30/2026 updated_at: 3/30/2026 keywords: - attribution - user - users - TLUserStore - TLUser - getAttributionUserId - getAttributionDisplayName - getAttributionUser - getReferencedUserIds - createCachedUserResolve - textLastEditedBy - UserRecordType - createUserId status: published date: 3/30/2026 --- 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. ## User store 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: ```tsx 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 (
) } ``` When no `users` prop is provided, the editor derives the current user from [user preferences](/sdk-features/user-preferences). With `useSync`, other users are resolved from collaborator presence. ### Resolving other users 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: ```tsx 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. ## How attribution works 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. ### Note shape attribution 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 label is rendered by the note shape util's `AttributionComponent` option, which you can override or hide with `NoteShapeUtil.configure`, passed through the `shapeUtils` prop. This applies on the canvas and in image exports: ```tsx import { NoteShapeUtil, Tldraw } from 'tldraw' // Hide the attribution badge ; // Or render your own ; (
✍️ {firstName}
), }), ]} /> ``` ## Reading attribution 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: ```tsx 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 {name} } ``` ## Custom shape attribution Add attribution tracking to your own shapes by storing a user ID in your shape's props and overriding [ShapeUtil#getReferencedUserIds](?): ```tsx import { ShapeUtil, T, TLBaseShape } from 'tldraw' type MyShape = TLBaseShape< 'my-shape', { createdBy: string | null } > class MyShapeUtil extends ShapeUtil { 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: ```tsx const userId = editor.getAttributionUserId() editor.createShape({ type: 'my-shape', props: { createdBy: userId, }, }) ``` ## Extensible user records Extend user records with custom metadata by passing validators to [createTLSchema](?): ```tsx 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. ## API reference | 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](/sdk-features/collaboration#User-identity) page. ## Related examples - [Attribution](/examples/users/attribution): basic attribution with a user switcher and attribution inspector - [Attribution timeline](/examples/users/attribution-timeline): timeline scrubber with per-user filtering