Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/bluesky-social/atproto/llms.txt

Use this file to discover all available pages before exploring further.

Overview

The @atproto/syntax package provides validation and parsing utilities for AT Protocol identifier formats including DIDs, handles, NSIDs, AT URIs, TIDs, and record keys.

Installation

npm install @atproto/syntax

DID (Decentralized Identifier)

Validation for W3C DID identifiers.

Validation Functions

import { isValidDid, ensureValidDid } from '@atproto/syntax'

// Type guard - returns boolean
if (isValidDid('did:plc:abc123xyz')) {
  console.log('Valid DID')
}

// Assertion - throws on invalid
try {
  ensureValidDid('did:plc:abc123xyz')
  console.log('DID is valid')
} catch (err) {
  console.error('Invalid DID:', err.message)
}
isValidDid
boolean
function isValidDid<I extends string>(input: I): input is I & DidString
Type guard that returns true if the input is a valid DID.
input
string
required
String to validate
ensureValidDid
void
function ensureValidDid<I extends string>(input: I): asserts input is I & DidString
Asserts that input is a valid DID. Throws InvalidDidError if invalid.

DID Format

type DidString<M extends string = string> = `did:${M}:${string}`
Constraints:
  • Must start with did:
  • Method name must be lowercase letters
  • Method-specific identifier can contain: a-zA-Z0-9._:%-
  • Cannot end with : or %
  • Maximum length: 2048 characters
Examples:
'did:plc:ewvi7nxzyoun6zhxrhs64oiz' // ✓ valid
'did:web:example.com'              // ✓ valid
'DID:plc:abc'                      // ✗ invalid (uppercase)
'did:plc:'                         // ✗ invalid (ends with :)

Handle

Validation for domain-based handles.

Validation Functions

import { 
  isValidHandle, 
  ensureValidHandle,
  normalizeHandle,
  isValidTld
} from '@atproto/syntax'

// Validate handle
if (isValidHandle('alice.bsky.social')) {
  console.log('Valid handle')
}

// Normalize (lowercase)
const normalized = normalizeHandle('Alice.BSKY.Social')
console.log(normalized) // 'alice.bsky.social'

// Validate TLD
if (!isValidTld('alice.local')) {
  console.log('Disallowed TLD')
}
isValidHandle
boolean
function isValidHandle<I extends string>(input: I): input is I & HandleString
Type guard that returns true if the input is a valid handle.
ensureValidHandle
void
function ensureValidHandle<I extends string>(input: I): asserts input is I & HandleString
Asserts that input is a valid handle. Throws InvalidHandleError if invalid.
normalizeHandle
string
function normalizeHandle(handle: string): string
Converts handle to lowercase (handles are case-insensitive).
isValidTld
boolean
function isValidTld(handle: string): boolean
Checks if handle’s TLD is allowed (not .local, .internal, etc.).

Handle Format

type HandleString = `${string}.${string}`
Constraints:
  • Valid domain name format (RFC-1035, RFC-3696)
  • Segments contain only: a-zA-Z0-9-
  • Segments cannot start or end with hyphen
  • Each segment: 1-63 characters
  • Total length: max 253 characters
  • At least 2 segments (e.g., user.domain)
  • TLD must start with letter
  • Case-insensitive
Disallowed TLDs: .local, .arpa, .invalid, .localhost, .internal, .example, .alt, .onion Examples:
'alice.bsky.social'  // ✓ valid
'bob.com'            // ✓ valid
'charlie'            // ✗ invalid (no TLD)
'alice.local'        // ✗ invalid (disallowed TLD)
'-invalid.com'       // ✗ invalid (starts with hyphen)

NSID (Namespaced Identifier)

Validation for Lexicon schema identifiers.

NSID Class

import { NSID } from '@atproto/syntax'

// Parse NSID
const nsid = NSID.parse('com.example.song')
console.log(nsid.authority) // 'example.com'
console.log(nsid.name)      // 'song'
console.log(nsid.toString()) // 'com.example.song'

// Create from parts
const nsid2 = NSID.create('example.com', 'post')
console.log(nsid2.toString()) // 'com.example.post'

// Validate
if (NSID.isValid('com.atproto.repo.createRecord')) {
  console.log('Valid NSID')
}
NSID.parse
NSID
static parse(input: string): NSID
Parses an NSID string. Throws InvalidNsidError if invalid.
NSID.create
NSID
static create(authority: string, name: string): NSID
Creates an NSID from authority and name.
authority
string
required
Domain authority (e.g., ‘example.com’)
name
string
required
Method name (e.g., ‘post’)
NSID.isValid
boolean
static isValid(nsid: string): boolean
Returns true if the string is a valid NSID.
Properties:
  • authority: string - Reversed domain (e.g., ‘example.com’)
  • name: string - Method name
  • segments: readonly string[] - All segments

Validation Functions

import { isValidNsid, ensureValidNsid } from '@atproto/syntax'

if (isValidNsid('com.example.song')) {
  console.log('Valid NSID')
}

ensureValidNsid('com.example.song') // throws if invalid

NSID Format

type NsidString = `${string}.${string}.${string}`
Constraints:
  • Reversed domain notation + name
  • Segments contain: a-zA-Z0-9-
  • Cannot start/end with hyphen
  • First segment cannot start with digit
  • Last segment (name) must be letters/digits only (no hyphens)
  • Each segment: 1-63 characters
  • Total length: max 317 characters (253 + 1 + 63)
  • At least 3 segments
Examples:
'com.atproto.repo.createRecord' // ✓ valid
'com.example.song'              // ✓ valid
'example.song'                  // ✗ invalid (only 2 segments)
'com.example.my-song'           // ✗ invalid (hyphen in name)

AT URI

Validation and parsing for AT Protocol URIs.

AtUri Class

import { AtUri } from '@atproto/syntax'

// Parse AT URI
const uri = new AtUri('at://bob.com/app.bsky.feed.post/3jzfcijpj2z2a')

console.log(uri.protocol)    // 'at:'
console.log(uri.host)        // 'bob.com'
console.log(uri.hostname)    // 'bob.com'
console.log(uri.collection)  // 'app.bsky.feed.post'
console.log(uri.rkey)        // '3jzfcijpj2z2a'
console.log(uri.pathname)    // '/app.bsky.feed.post/3jzfcijpj2z2a'

// Get DID (if host is DID)
const uri2 = new AtUri('at://did:plc:abc/app.bsky.feed.post/123')
console.log(uri2.did) // 'did:plc:abc'

// Construct from parts
const uri3 = AtUri.make('alice.bsky.social', 'app.bsky.feed.post', '3k2akjd')
console.log(uri3.toString()) 
// 'at://alice.bsky.social/app.bsky.feed.post/3k2akjd'

// With query params
const uri4 = new AtUri('at://bob.com/collection/rkey?param=value')
console.log(uri4.searchParams.get('param')) // 'value'
constructor
AtUri
constructor(uri: string, base?: string | AtUri)
Parses an AT URI. Optionally resolves relative URIs against a base.
uri
string
required
AT URI to parse
base
string | AtUri
Base URI for resolving relative references
AtUri.make
AtUri
static make(handleOrDid: string, collection?: string, rkey?: string): AtUri
Constructs an AT URI from parts.
handleOrDid
string
required
Handle or DID
collection
string
Collection NSID
rkey
string
Record key
Properties:
  • protocol: string - Always 'at:'
  • origin: string - at://{host}
  • host: string - DID or handle
  • hostname: string - Alias for host
  • pathname: string - Path portion
  • collection: string - Collection NSID
  • rkey: string - Record key
  • hash: string - Fragment
  • searchParams: URLSearchParams - Query parameters
  • did: DidString - DID (throws if host is not DID)
Methods:
  • toString(): AtUriString - Converts to string

AT URI Format

type AtUriString = `at://${string}`
Format:
at://authority/collection/rkey?query#fragment
Examples:
'at://bob.com/app.bsky.feed.post/123'               // ✓ valid
'at://did:plc:abc/app.bsky.actor.profile/self'     // ✓ valid
'at://alice.com'                                    // ✓ valid (repo only)
'at://bob.com/app.bsky.feed.post/123?cursor=xyz'   // ✓ valid (with query)

TID (Timestamp Identifier)

Validation for timestamp-based identifiers.
import { isValidTid, ensureValidTid } from '@atproto/syntax'

if (isValidTid('3jzfcijpj2z2a')) {
  console.log('Valid TID')
}

ensureValidTid('3jzfcijpj2z2a') // throws if invalid
isValidTid
boolean
function isValidTid<I extends string>(input: I): input is I & TidString
Type guard for TID validation.
ensureValidTid
void
function ensureValidTid<I extends string>(input: I): asserts input is I & TidString
Asserts input is a valid TID. Throws InvalidTidError if invalid.

TID Format

type TidString = string
Constraints:
  • Exactly 13 characters
  • Base32-sortable encoding
  • First character: [234567abcdefghij]
  • Remaining characters: [234567abcdefghijklmnopqrstuvwxyz]
Examples:
'3jzfcijpj2z2a' // ✓ valid
'3k2akjd8fj3la' // ✓ valid
'1234567890123' // ✗ invalid (starts with 1)
'3jzfcijpj2z'   // ✗ invalid (too short)

Record Key

Validation for repository record keys.
import { isValidRecordKey, ensureValidRecordKey } from '@atproto/syntax'

if (isValidRecordKey('3jzfcijpj2z2a')) {
  console.log('Valid record key')
}

if (isValidRecordKey('self')) {
  console.log('Valid literal key')
}

ensureValidRecordKey('my-record-123') // throws if invalid
isValidRecordKey
boolean
function isValidRecordKey<I extends string>(input: I): input is I & RecordKeyString
Type guard for record key validation.
ensureValidRecordKey
void
function ensureValidRecordKey<I extends string>(input: I): asserts input is I & RecordKeyString
Asserts input is a valid record key. Throws InvalidRecordKeyError if invalid.

Record Key Format

type RecordKeyString = string
Constraints:
  • Allowed characters: a-zA-Z0-9_~.:-
  • Length: 1-512 characters
  • Cannot be . or ..
Examples:
'3jzfcijpj2z2a'     // ✓ valid (TID)
'self'              // ✓ valid (literal)
'my-record'         // ✓ valid
'2023-01-01'        // ✓ valid
'.'                 // ✗ invalid
'..'                // ✗ invalid
'invalid/key'       // ✗ invalid (slash not allowed)

Datetime

Validation for ISO 8601 datetime strings.
import { isValidDatetime, ensureValidDatetime } from '@atproto/syntax'

const dt = new Date().toISOString()

if (isValidDatetime(dt)) {
  console.log('Valid datetime')
}

ensureValidDatetime('2024-01-15T12:30:00.000Z') // throws if invalid
isValidDatetime
boolean
function isValidDatetime(input: string): boolean
Returns true if input is a valid ISO 8601 datetime.
ensureValidDatetime
void
function ensureValidDatetime(input: string): void
Asserts input is a valid ISO 8601 datetime. Throws if invalid.

Complete Example

import {
  isValidDid,
  isValidHandle,
  NSID,
  AtUri,
  isValidTid,
  isValidRecordKey
} from '@atproto/syntax'

// Validate identifiers
const did = 'did:plc:ewvi7nxzyoun6zhxrhs64oiz'
const handle = 'alice.bsky.social'

if (isValidDid(did) && isValidHandle(handle)) {
  console.log('Valid identifiers')
}

// Parse NSID
const nsid = NSID.parse('app.bsky.feed.post')
console.log(`Authority: ${nsid.authority}, Name: ${nsid.name}`)

// Parse AT URI
const uri = new AtUri(`at://${did}/${nsid.toString()}/3jzfcijpj2z2a`)
console.log('URI components:', {
  host: uri.host,
  collection: uri.collection,
  rkey: uri.rkey
})

// Validate record key
const rkey = uri.rkey
if (isValidTid(rkey)) {
  console.log('Record key is a TID')
} else if (isValidRecordKey(rkey)) {
  console.log('Record key is valid')
}

// Construct new URI
const newUri = AtUri.make(
  handle,
  'app.bsky.actor.profile',
  'self'
)
console.log('Profile URI:', newUri.toString())
// at://alice.bsky.social/app.bsky.actor.profile/self

// Work with query parameters
const queryUri = new AtUri(
  `at://${handle}/app.bsky.feed.post?cursor=abc&limit=50`
)
console.log('Cursor:', queryUri.searchParams.get('cursor'))
console.log('Limit:', queryUri.searchParams.get('limit'))

Error Classes

All validation functions throw specific error types:
  • InvalidDidError
  • InvalidHandleError
  • InvalidNsidError
  • InvalidTidError
  • InvalidRecordKeyError
  • InvalidDatetimeError
import { InvalidDidError, ensureValidDid } from '@atproto/syntax'

try {
  ensureValidDid('not-a-did')
} catch (err) {
  if (err instanceof InvalidDidError) {
    console.error('DID validation failed:', err.message)
  }
}

Additional Resources

Build docs developers (and LLMs) love