packages/tlschema/DOCS.md
The schema package defines the type system, data structures, validation, and migrations for tldraw's persisted data. It provides a complete, type-safe, and version-aware data model that powers the tldraw editor.
@tldraw/tlschema is the foundational package that defines how tldraw stores and manages data. It contains:
You'll use this package when creating custom shapes, defining your own data schemas, or when you need to work with tldraw's data structures directly.
A schema defines the structure of your tldraw store. It specifies what types of records can exist, how they're validated, and how they evolve over time.
import { createTLSchema, defaultShapeSchemas } from '@tldraw/tlschema'
// Create a schema with default shapes
const schema = createTLSchema({
shapes: defaultShapeSchemas,
})
Records are the individual pieces of data stored in tldraw. Every record has a type, an ID, and properties specific to that type:
import { TLShape, TLPage, TLAsset } from '@tldraw/tlschema'
// All records extend BaseRecord
const shape: TLShape = {
id: 'shape:abc123',
typeName: 'shape',
type: 'geo',
x: 100,
y: 200,
rotation: 0,
// ... other properties
}
Style properties are special properties that can be applied across multiple shapes and persist for new shape creation:
import { StyleProp } from '@tldraw/tlschema'
import { T } from '@tldraw/validate'
// Define a custom style property
const MyCustomStyle = StyleProp.define('myapp:custom', {
defaultValue: 'default',
type: T.string,
})
You create schemas by combining shape configurations, bindings, and migrations:
import { createTLSchema, defaultShapeSchemas, defaultBindingSchemas } from '@tldraw/tlschema'
const schema = createTLSchema({
shapes: {
...defaultShapeSchemas,
// Add custom shapes here
},
bindings: defaultBindingSchemas,
})
Shape records are the most common type you'll work with. Every shape extends TLBaseShape:
import { createShapeValidator } from '@tldraw/tlschema'
import { T } from '@tldraw/validate'
const CUSTOM_TYPE = 'custom'
// Define a custom shape type
declare module '@tldraw/tlschema' {
export interface TLGlobalShapePropsMap {
[CUSTOM_TYPE]: MyCustomShapeProps
}
}
interface MyCustomShapeProps {
width: number
height: number
color: string
}
type MyCustomShape = TLShape<typeof CUSTOM_TYPE>
// Create validation for your shape
const customShapeValidator = createShapeValidator(CUSTOM_TYPE, {
width: T.number,
height: T.number,
color: T.string,
})
Once you have a schema, you use it to create a store:
import { Store } from '@tldraw/store'
import { TLStoreProps } from '@tldraw/tlschema'
const store = new Store({
schema,
props: {
defaultName: 'My Drawing',
assets: myAssetStore, // Your asset storage implementation
},
})
When creating custom shapes, follow this pattern for complete integration:
import {
createShapeValidator,
createShapePropsMigrationSequence,
RecordProps,
} from '@tldraw/tlschema'
import { DefaultColorStyle } from '@tldraw/tlschema'
const MY_SHAPE_TYPE = 'myshape'
// 1. Define the shape
declare module '@tldraw/tlschema' {
export interface TLGlobalShapePropsMap {
[MY_SHAPE_TYPE]: MyShapeProps
}
}
interface MyShapeProps {
color: typeof DefaultColorStyle // Use existing style
width: number
height: number
customData: string
}
type MyShape = TLShape<typeof MY_SHAPE_TYPE>
// 2. Create props validation
const myShapeProps: RecordProps<MyShape> = {
color: DefaultColorStyle,
width: T.number,
height: T.number,
customData: T.string,
}
// 3. Define migrations for schema evolution
const myShapeMigrations = createShapePropsMigrationSequence({
sequenceId: 'com.myapp.shape.myshape',
sequence: [
{
id: 'com.myapp.shape.myshape/1.0.0',
up: (props) => props, // Initial version
down: (props) => props,
},
],
})
// 4. Add to schema
const schema = createTLSchema({
shapes: {
...defaultShapeSchemas,
myshape: {
props: myShapeProps,
migrations: myShapeMigrations,
},
},
})
Style properties enable consistent styling across shapes and remember the last used value:
import { StyleProp, EnumStyleProp } from '@tldraw/tlschema'
import { T } from '@tldraw/validate'
// Free-form style property
const MyWidthStyle = StyleProp.define('myapp:width', {
defaultValue: 2,
type: T.number,
})
// Enum-based style property
const MyPatternStyle = StyleProp.defineEnum('myapp:pattern', {
defaultValue: 'solid',
values: ['solid', 'dashed', 'dotted'],
})
// Use in shape props
interface MyShapeProps {
width: typeof MyWidthStyle
pattern: typeof MyPatternStyle
// other props...
}
Assets represent external resources like images, videos, or bookmarks:
import { TLImageAsset, TLAssetStore } from '@tldraw/tlschema'
// Implement asset storage
const assetStore: TLAssetStore = {
async upload(asset, file) {
// Upload file to your storage service
const src = await uploadToStorage(file)
return { src }
},
async resolve(asset, context) {
// Resolve asset URL for rendering
return asset.props.src
},
async remove(assetIds) {
// Clean up removed assets
await deleteFromStorage(assetIds)
},
}
Migrations handle schema evolution as your application develops:
import { createShapePropsMigrationSequence } from '@tldraw/tlschema'
const migrations = createShapePropsMigrationSequence({
sequenceId: 'com.myapp.shape.custom',
sequence: [
{
id: 'com.myapp.shape.custom/1.1.0',
up: (props) => {
// Add new property with default value
return { ...props, newProperty: 'default' }
},
down: ({ newProperty, ...props }) => {
// Remove property for backward compatibility
return props
},
},
{
id: 'com.myapp.shape.custom/1.2.0',
up: (props) => {
// Rename property
return {
...props,
renamedProperty: props.oldProperty,
oldProperty: undefined,
}
},
down: (props) => {
return {
...props,
oldProperty: props.renamedProperty,
renamedProperty: undefined,
}
},
},
],
})
tldraw includes several built-in shape types:
import {
TLGeoShape, // Rectangles, ellipses, triangles, etc.
TLTextShape, // Text with rich formatting
TLDrawShape, // Freehand drawing paths
TLArrowShape, // Arrows with optional binding to shapes
TLLineShape, // Multi-point lines and splines
TLImageShape, // Raster images
TLVideoShape, // Video files
TLNoteShape, // Sticky notes
TLBookmarkShape, // Website bookmarks
TLEmbedShape, // Embedded content (YouTube, Figma, etc.)
TLFrameShape, // Frames for grouping content
TLGroupShape, // Groups for organizing shapes
TLHighlightShape, // Highlighting tool strokes
} from '@tldraw/tlschema'
All shapes share common base properties from TLBaseShape:
interface TLBaseShape<Type, Props> {
id: TLShapeId
type: Type
x: number // Position X
y: number // Position Y
rotation: number // Rotation in radians
index: IndexKey // Fractional index for ordering
parentId: TLParentId // Parent page or shape
isLocked: boolean // Whether shape can be selected
opacity: TLOpacityType // Transparency (0-1)
props: Props // Shape-specific properties
meta: JsonObject // User-defined metadata
}
Geometric shapes support various styles and configurations:
import { TLGeoShape, GeoShapeGeoStyle } from '@tldraw/tlschema'
// Geo shapes can be rectangles, ellipses, triangles, etc.
const geoShape: TLGeoShape = {
// ... base properties
props: {
geo: GeoShapeGeoStyle, // 'rectangle', 'ellipse', 'triangle', etc.
w: 100, // Width
h: 80, // Height
color: 'blue', // Color style
fill: 'solid', // Fill style
dash: 'solid', // Dash style
size: 'm', // Size style
richText: null, // Optional text content
},
}
All records are validated at runtime to ensure data integrity:
import { T } from '@tldraw/validate'
import { createShapeValidator } from '@tldraw/tlschema'
// Validation happens automatically when records enter the store
const validator = createShapeValidator('myshape', {
width: T.number.check((n) => n > 0), // Custom validation
height: T.number.check((n) => n > 0),
color: T.string,
})
When validation fails, you can handle errors gracefully:
try {
store.put([invalidRecord])
} catch (error) {
if (error instanceof ValidationError) {
console.log('Validation failed:', error.message)
// Handle validation error appropriately
}
}
Record IDs are strongly typed to prevent mixing different record types:
import { TLShapeId, TLPageId, createShapeId } from '@tldraw/tlschema'
// IDs are branded types - compiler prevents mixing them up
const shapeId: TLShapeId = createShapeId()
const pageId: TLPageId = 'page:123' // TypeScript error if you use wrong format
The schema integrates with the store system to provide reactive data management:
import { Store } from '@tldraw/store'
import { TLStoreProps, createTLSchema } from '@tldraw/tlschema'
const schema = createTLSchema()
const store = new Store({
schema,
props: {
defaultName: 'Untitled',
assets: assetStore,
onMount: (editor) => {
// Initialize when editor mounts
console.log('Editor mounted with store')
},
},
})
The store provides reactive access to records:
import { track } from '@tldraw/state'
// This function will re-run when shapes change
const ShapeCounter = track(() => {
const shapes = store.query.records('shape').get()
return `Total shapes: ${shapes.length}`
})
validateUsingKnownGoodVersion() when you know data is already validdevFreeze() helps catch mutation bugs in developmentYou can inspect your schema to understand its configuration:
const schema = createTLSchema()
// Examine record types
console.log('Record types:', Object.keys(schema.types))
// Check validation for specific record type
const shapeValidator = schema.types.shape
console.log('Shape validator:', shapeValidator)
When migrations fail, examine the migration sequence:
// Check migration history
const migrations = schema.sortedMigrations
console.log(
'Migration sequence:',
migrations.map((m) => m.id)
)
// Test individual migrations
try {
const migrated = migrator.migrateStoreSnapshot({
schema: oldSchema,
store: snapshot,
})
console.log('Migration successful')
} catch (error) {
console.error('Migration failed:', error)
}
When records fail validation, examine the validation path:
import { T } from '@tldraw/validate'
try {
shapeValidator.validate(invalidShape)
} catch (error) {
console.log('Validation path:', error.path)
console.log('Validation message:', error.message)
console.log('Invalid value:', error.value)
}
Many shapes reference assets for their content:
interface MediaShapeProps {
assetId: TLAssetId | null
width: number
height: number
crop?: TLShapeCrop // Optional cropping info
}
// Usage pattern
const imageShape: TLImageShape = {
// ... base properties
props: {
assetId: 'asset:image123',
w: 200,
h: 150,
crop: {
topLeft: { x: 0, y: 0 },
bottomRight: { x: 1, y: 1 },
},
},
}
Arrows can bind to other shapes:
import { TLArrowBinding } from '@tldraw/tlschema'
const arrowBinding: TLArrowBinding = {
id: 'binding:abc123',
typeName: 'binding',
type: 'arrow',
fromId: 'shape:arrow1', // Arrow shape ID
toId: 'shape:rectangle1', // Target shape ID
props: {
terminal: 'end', // 'start' or 'end'
normalizedAnchor: { x: 0.5, y: 0.5 }, // Position on target
isExact: false, // Whether position is exact
isPrecise: true, // Whether binding is precise
},
}
Shapes can contain formatted text:
import { TLRichText, toRichText } from '@tldraw/tlschema'
const richText: TLRichText = toRichText('Hello **bold** text')
const textShape: TLTextShape = {
// ... base properties
props: {
color: 'black',
size: 'm',
font: 'draw',
textAlign: 'start',
richText: richText,
autoSize: true,
scale: 1,
},
}
Extend the built-in shape system:
// Add custom shapes to default schemas
const customSchema = createTLSchema({
shapes: {
...defaultShapeSchemas,
myCustomShape: customShapeConfig,
},
})
Create new types of shape relationships:
const CUSTOM_TYPE = 'custom'
// Define custom binding types
declare module '@tldraw/tlschema' {
export interface TLGlobalBindingPropsMap {
[CUSTOM_TYPE]: MyBindingProps
}
}
type MyCustomBinding = TLBinding<typeof CUSTOM_TYPE>
const customBindingConfig = {
props: myBindingProps,
migrations: myBindingMigrations,
}
Implement custom asset storage backends:
const customAssetStore: TLAssetStore = {
async upload(asset, file) {
return await myCloudStorage.upload(file)
},
async resolve(asset, context) {
return await myCloudStorage.getUrl(asset.props.src, context)
},
async remove(assetIds) {
await Promise.all(assetIds.map((id) => myCloudStorage.delete(id)))
},
}
The tlschema package provides a robust foundation for building applications with tldraw. By following these patterns and understanding the core concepts, you can create custom shapes, manage data effectively, and build experiences that scale with your users' needs.