Skip to main content
Records are individual entries in your collections. You can create, read, update, and delete records using the JavaScript SDK.

Creating records

Basic creation

const collection = $app.findCollectionByNameOrId("posts")

const record = new Record(collection, {
  title: "Getting Started with PocketBase",
  content: "PocketBase is an open source backend...",
  author: "user_id_here",
  status: "draft",
})

$app.save(record)

Using set method

const collection = $app.findCollectionByNameOrId("posts")
const record = new Record(collection)

record.set("title", "My New Post")
record.set("content", "This is the content")
record.set("author", authRecord.id)
record.set("status", "published")

$app.save(record)

With file uploads

onRecordCreateRequest((e) => {
  if (e.collection.name !== "posts") {
    return e.next()
  }
  
  const form = e.requestInfo().data
  const file = form.featured_image
  
  if (file) {
    // File is automatically handled by PocketBase
    // Just ensure your collection has a file field named "featured_image"
  }
  
  return e.next()
}, "posts")

Reading records

Find by ID

try {
  const record = $app.findRecordById("posts", "RECORD_ID")
  console.log(record.getString("title"))
} catch (err) {
  console.log("Record not found")
}

Find by field value

// Find first match
const user = $app.findFirstRecordByData("users", "email", "test@example.com")

// Find all matches
const records = arrayOf(new Record())
$app.findAllRecordsByData("posts", "status", "published", records)

for (let record of records) {
  console.log(record.getString("title"))
}

Using query builder

const records = arrayOf(new Record())

$app.recordQuery("posts")
  .andWhere($dbx.hashExp({
    "status": "published",
  }))
  .andWhere($dbx.like("title", "JavaScript"))
  .orderBy("created DESC")
  .limit(20)
  .all(records)

console.log("Found", records.length, "posts")

Query with relations

const records = arrayOf(new Record())

$app.recordQuery("posts")
  .andWhere($dbx.hashExp({"status": "published"}))
  .orderBy("created DESC")
  .limit(10)
  .all(records)

// Expand author relation
for (let record of records) {
  const authorId = record.getString("author")
  const author = $app.findRecordById("users", authorId)
  console.log(record.getString("title"), "by", author.getString("username"))
}

Updating records

Update fields

const record = $app.findRecordById("posts", "RECORD_ID")

record.set("title", "Updated Title")
record.set("content", "Updated content here")
record.set("status", "published")

$app.save(record)

Partial updates

const record = $app.findRecordById("posts", "RECORD_ID")

// Only update specific fields
record.set("view_count", record.getInt("view_count") + 1)

$app.save(record)

Update with validation

const record = $app.findRecordById("posts", "RECORD_ID")

const newTitle = "New Title"
if (newTitle.length < 3) {
  throw new BadRequestError("Title must be at least 3 characters")
}

record.set("title", newTitle)
$app.save(record)

Deleting records

Delete single record

const record = $app.findRecordById("posts", "RECORD_ID")
$app.delete(record)

Delete with cascade

// When you delete a record, related records are handled
// based on the cascadeDelete setting in relation fields

const user = $app.findRecordById("users", "USER_ID")

// If posts have a relation to users with cascadeDelete: true,
// all posts by this user will be deleted automatically
$app.delete(user)

Bulk delete

const records = arrayOf(new Record())

// Find all draft posts older than 30 days
const cutoff = new DateTime()
cutoff.addDate(0, 0, -30)

$app.recordQuery("posts")
  .andWhere($dbx.hashExp({"status": "draft"}))
  .andWhere($dbx.exp("created < {:cutoff}", {cutoff: cutoff.string()}))
  .all(records)

for (let record of records) {
  $app.delete(record)
}

console.log("Deleted", records.length, "old drafts")

Accessing field values

Type-safe getters

const record = $app.findRecordById("posts", "RECORD_ID")

// String fields
const title = record.getString("title")
const content = record.getString("content")

// Number fields
const viewCount = record.getInt("view_count")
const price = record.getFloat("price")

// Boolean fields
const featured = record.getBool("featured")

// Date fields
const publishedAt = record.getDateTime("published_at")

// JSON fields
const metadata = record.get("metadata")

Working with arrays

const record = $app.findRecordById("posts", "RECORD_ID")

// Select fields (arrays)
const tags = record.get("tags") || []
for (let tag of tags) {
  console.log("Tag:", tag)
}

// Relation fields (arrays of IDs)
const categoryIds = record.get("categories") || []
for (let categoryId of categoryIds) {
  const category = $app.findRecordById("categories", categoryId)
  console.log("Category:", category.getString("name"))
}

File handling

Get file URLs

const record = $app.findRecordById("posts", "RECORD_ID")
const filename = record.getString("featured_image")

if (filename) {
  const url = $app.fileUrl(record, filename)
  console.log("Image URL:", url)
  
  // Get thumbnail URL
  const thumbUrl = $app.fileUrl(record, filename, "200x200")
  console.log("Thumbnail URL:", thumbUrl)
}

Upload files programmatically

const collection = $app.findCollectionByNameOrId("posts")
const record = new Record(collection)

record.set("title", "Post with Image")

// Create file from local path
const file = $filesystem.fileFromPath("/path/to/image.jpg")
record.set("featured_image", file)

$app.save(record)

Download and attach external files

const collection = $app.findCollectionByNameOrId("posts")
const record = new Record(collection)

record.set("title", "Post with External Image")

// Download file from URL
const file = $filesystem.fileFromURL("https://example.com/image.jpg", 30)
record.set("featured_image", file)

$app.save(record)

Working with auth records

Create user

const collection = $app.findCollectionByNameOrId("users")
const user = new Record(collection)

user.set("email", "newuser@example.com")
user.set("username", "newuser")
user.setPassword("securePassword123")
user.set("emailVisibility", true)
user.set("verified", false)

$app.save(user)

// Send verification email
$mails.sendRecordVerification($app, user)

Update password

const user = $app.findAuthRecordByEmail("users", "user@example.com")

user.setPassword("newSecurePassword456")
$app.save(user)

Verify email

const user = $app.findAuthRecordByEmail("users", "user@example.com")

user.set("verified", true)
$app.save(user)

Validation

Manual validation

onRecordCreate((e) => {
  if (e.collection.name !== "posts") {
    return e.next()
  }
  
  const record = e.record
  
  // Custom validation
  const title = record.getString("title")
  if (title.includes("spam")) {
    throw new BadRequestError("Title contains forbidden words")
  }
  
  // Check for duplicates
  const existing = $app.findFirstRecordByData(
    "posts",
    "slug",
    record.getString("slug")
  )
  
  if (existing) {
    throw new BadRequestError("A post with this slug already exists")
  }
  
  return e.next()
}, "posts")

Using validation errors

routerAdd("POST", "/api/posts/validate", (e) => {
  const data = e.requestInfo().data
  
  const errors = {}
  
  if (!data.title || data.title.length < 3) {
    errors.title = new ValidationError("validation_min_length", "Title is too short")
  }
  
  if (!data.content || data.content.length < 10) {
    errors.content = new ValidationError("validation_required", "Content is required")
  }
  
  if (Object.keys(errors).length > 0) {
    throw new BadRequestError("Validation failed", errors)
  }
  
  return e.json(200, {valid: true})
})

Examples

Increment view counter

onRecordViewRequest((e) => {
  if (e.collection.name !== "posts") {
    return e.next()
  }
  
  // Increment view count
  const record = e.record
  const currentCount = record.getInt("view_count") || 0
  record.set("view_count", currentCount + 1)
  
  $app.save(record)
  
  return e.next()
}, "posts")

Auto-generate slug

onRecordCreate((e) => {
  if (e.collection.name !== "posts") {
    return e.next()
  }
  
  const record = e.record
  const title = record.getString("title")
  
  // Generate slug from title
  const slug = title
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-|-$/g, "")
  
  record.set("slug", slug)
  
  return e.next()
}, "posts")

Copy record

routerAdd("POST", "/api/posts/:id/duplicate", (e) => {
  const id = e.request.pathValue("id")
  const original = $app.findRecordById("posts", id)
  
  const collection = $app.findCollectionByNameOrId("posts")
  const copy = new Record(collection)
  
  // Copy all fields except ID and timestamps
  copy.set("title", original.getString("title") + " (Copy)")
  copy.set("content", original.getString("content"))
  copy.set("author", original.getString("author"))
  copy.set("status", "draft")
  
  $app.save(copy)
  
  return e.json(200, copy)
}, $apis.requireAuth())

Batch create

routerAdd("POST", "/api/posts/batch", (e) => {
  const data = e.requestInfo().data
  const posts = data.posts || []
  
  const collection = $app.findCollectionByNameOrId("posts")
  const created = []
  
  $app.runInTransaction((txApp) => {
    for (let postData of posts) {
      const record = new Record(collection)
      record.set("title", postData.title)
      record.set("content", postData.content)
      record.set("author", e.requestInfo().auth.id)
      record.set("status", "draft")
      
      txApp.save(record)
      created.push(record)
    }
    return null
  })
  
  return e.json(200, {
    created: created.length,
    records: created,
  })
}, $apis.requireAuth())