Back to Tldraw

Attribution

apps/docs/content/sdk-features/attribution.mdx

5.4.06.5 KB
Original Source

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 (
		<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.

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.

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 <span>{name}</span>
}

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<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:

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

SymbolDescription
Editor#getAttributionUserIdGet the current user's ID for stamping shapes. Returns string | null
Editor#getAttributionDisplayNameResolve a display name from a user ID. Returns string | null
Editor#getAttributionUserResolve a full TLUser record from a user ID
TLUserStoreInterface for connecting to your auth/user system
TLUserA user record in the store
UserRecordTypeThe default user record type
createUserIdCreate a typed user ID
createCachedUserResolveCreate a cached resolve function for TLUserStore
createUserRecordTypeBuild a user record type with custom meta validators

For setting up user identity in multiplayer, see the Collaboration page.