Skip to main content
PocketBase provides a built-in API endpoint for serving files from records. The system handles content type detection, range requests, caching, and security headers automatically.

Download endpoint

Files are served through the following endpoint:
GET /api/files/{collection}/{recordId}/{filename}

Path parameters

  • collection - The collection name or ID
  • recordId - The record ID
  • filename - The exact filename stored in the record

Example request

curl https://your-app.com/api/files/users/abc123/avatar_kj3n4k5l6m.png

Query parameters

You can modify the download behavior using query parameters:

Thumbnail request

Request a specific thumbnail size for image files:
# Get 100x100 thumbnail
GET /api/files/users/abc123/photo_x1y2z3.jpg?thumb=100x100

# Get 200x200 thumbnail cropped from top
GET /api/files/users/abc123/photo_x1y2z3.jpg?thumb=200x200t

# Fit inside 300x300 without cropping
GET /api/files/users/abc123/photo_x1y2z3.jpg?thumb=300x300f
Thumbnails are generated on-demand on the first request and cached for subsequent requests. They’re stored at {collection}/{recordId}/thumbs_{filename}/{thumbSize}_{filename}.

Force download

Force the browser to download the file instead of displaying it inline:
GET /api/files/users/abc123/report.pdf?download=1
This sets Content-Disposition: attachment instead of inline.

Protected file access

For protected files, include the file token:
GET /api/files/users/abc123/private_doc.pdf?token=eyJhbGc...

Response headers

PocketBase sets appropriate headers for file serving:
HTTP/1.1 200 OK
Content-Type: image/png
Content-Disposition: inline; filename=avatar_kj3n4k5l6m.png
Content-Length: 45678
Cache-Control: max-age=2592000, stale-while-revalidate=86400
Content-Security-Policy: default-src 'none'; media-src 'self'; style-src 'unsafe-inline'; sandbox
ETag: "d41d8cd98f00b204e9800998ecf8427e"
Last-Modified: Mon, 02 Jan 2006 15:04:05 GMT

Header details

  • Content-Type - Automatically detected from file content
  • Content-Disposition - inline for viewable files, attachment for downloads
  • Cache-Control - Valid for 30 days with background revalidation
  • Content-Security-Policy - Restrictive CSP for security
  • ETag - For cache validation
  • Last-Modified - File modification timestamp
The X-Frame-Options header is removed for file endpoints to allow embedding files in iframes.

Protected files

When a file field has Protected: true, users must provide a valid file token to access files.

Getting a file token

Authenticated users can request a file token:
POST /api/files/token
Authorization: Bearer {authToken}
Response:
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Using the file token

Append the token to file URLs:
const token = await pb.files.getToken();
const fileUrl = pb.files.getUrl(record, 'document', {'token': token});

// Result:
// https://your-app.com/api/files/docs/xyz789/file.pdf?token=eyJhbG...
File tokens are time-limited. Protected files also respect collection view rules - even with a valid token, users can only access files from records they have permission to view.

Implementation details

The download endpoint implementation (from apis/file.go:84):
func (api *fileApi) download(e *core.RequestEvent) error {
    // Find collection
    collection, err := e.App.FindCachedCollectionByNameOrId(
        e.Request.PathValue("collection"),
    )
    if err != nil {
        return e.NotFoundError("", nil)
    }
    
    // Find record
    recordId := e.Request.PathValue("recordId")
    record, err := e.App.FindRecordById(collection, recordId)
    if err != nil {
        return e.NotFoundError("", err)
    }
    
    // Find file field
    filename := e.Request.PathValue("filename")
    fileField := record.FindFileFieldByFile(filename)
    if fileField == nil {
        return e.NotFoundError("", nil)
    }
    
    // Check authorization for protected files
    if fileField.Protected {
        // Verify token and view permissions
        // ...
    }
    
    // Initialize filesystem
    fsys, err := e.App.NewFilesystem()
    if err != nil {
        return e.InternalServerError("Filesystem initialization failure.", err)
    }
    defer fsys.Close()
    
    // Serve file
    return fsys.Serve(e.Response, e.Request, originalPath, filename)
}

Thumbnail generation

Thumbnails are generated using the filesystem.CreateThumb method (from filesystem.go:489):
func (s *System) CreateThumb(
    originalKey string,
    thumbKey string,
    thumbSize string,
) error {
    // Parse thumb size (e.g., "100x100", "200x200t")
    sizeParts := ThumbSizeRegex.FindStringSubmatch(thumbSize)
    width, _ := strconv.Atoi(sizeParts[1])
    height, _ := strconv.Atoi(sizeParts[2])
    resizeType := sizeParts[3] // "", "t", "b", "f"
    
    // Load original image
    r, err := s.GetReader(originalKey)
    if err != nil {
        return err
    }
    defer r.Close()
    
    // Decode image
    img, err := imaging.Decode(r, imaging.AutoOrientation(true))
    if err != nil {
        return err
    }
    
    // Resize based on type
    var thumbImg *image.NRGBA
    switch resizeType {
    case "f": // fit
        thumbImg = imaging.Fit(img, width, height, imaging.Linear)
    case "t": // crop from top
        thumbImg = imaging.Fill(img, width, height, imaging.Top, imaging.Linear)
    case "b": // crop from bottom
        thumbImg = imaging.Fill(img, width, height, imaging.Bottom, imaging.Linear)
    default: // crop from center
        thumbImg = imaging.Fill(img, width, height, imaging.Center, imaging.Linear)
    }
    
    // Upload thumbnail
    // ...
}

Supported image formats

Thumbnail generation works with:
  • image/png
  • image/jpg / image/jpeg
  • image/gif
  • image/webp
WebP images are decoded for thumbnail generation, but the thumbnails are saved as PNG to ensure broad compatibility.

Serving files programmatically

You can serve files in custom endpoints using the filesystem:
func customFileEndpoint(e *core.RequestEvent) error {
    // Initialize filesystem
    fsys, err := e.App.NewFilesystem()
    if err != nil {
        return err
    }
    defer fsys.Close()
    
    // Serve file
    fileKey := "custom/path/file.pdf"
    displayName := "document.pdf"
    
    return fsys.Serve(e.Response, e.Request, fileKey, displayName)
}

Getting file reader

For more control, get a file reader:
fsys, err := app.NewFilesystem()
if err != nil {
    return err
}
defer fsys.Close()

// Get reader
reader, err := fsys.GetReader("path/to/file.pdf")
if err != nil {
    return err
}
defer reader.Close()

// Access file properties
contentType := reader.ContentType() // "application/pdf"
modTime := reader.ModTime()         // Last modified time
size := reader.Size()               // File size in bytes

// Read content
data, err := io.ReadAll(reader)

Checking file existence

fsys, err := app.NewFilesystem()
if err != nil {
    return err
}
defer fsys.Close()

exists, err := fsys.Exists("path/to/file.txt")
if err != nil {
    return err
}

if exists {
    // File exists
}

Getting file attributes

attrs, err := fsys.Attributes("path/to/file.txt")
if err != nil {
    return err
}

fmt.Println("Content-Type:", attrs.ContentType)
fmt.Println("Size:", attrs.Size)
fmt.Println("Modified:", attrs.ModTime)
fmt.Println("MD5:", attrs.MD5)
fmt.Println("Metadata:", attrs.Metadata)

Range requests

PocketBase supports HTTP range requests for partial file downloads:
# Request first 1024 bytes
curl -H "Range: bytes=0-1023" \
  https://your-app.com/api/files/videos/abc/video_xyz.mp4

# Request from byte 1024 to end
curl -H "Range: bytes=1024-" \
  https://your-app.com/api/files/videos/abc/video_xyz.mp4
Response:
HTTP/1.1 206 Partial Content
Content-Type: video/mp4
Content-Range: bytes 0-1023/45678
Content-Length: 1024
Range requests are handled automatically by http.ServeContent, enabling video streaming and resumable downloads.

Inline content types

These content types are served with Content-Disposition: inline by default:
var inlineServeContentTypes = []string{
    // Images
    "image/png", "image/jpg", "image/jpeg", "image/gif",
    "image/webp", "image/x-icon", "image/bmp",
    
    // Videos
    "video/webm", "video/mp4", "video/3gpp",
    "video/quicktime", "video/x-ms-wmv",
    
    // Audio
    "audio/basic", "audio/aiff", "audio/mpeg", "audio/midi",
    "audio/mp3", "audio/wave", "audio/wav", "audio/x-wav",
    "audio/x-mpeg", "audio/x-m4a", "audio/aac",
    
    // Documents
    "application/pdf", "application/x-pdf",
}
All other types are served as attachment (download) by default.

Error handling

The download endpoint returns appropriate error responses:
// Collection not found
// 404 Not Found

// Record not found
// 404 Not Found

// File not in record
// 404 Not Found

// Protected file without valid token
// 404 Not Found (not 403 to avoid leaking file existence)

// Filesystem error
// 500 Internal Server Error

Complete example

Here’s a complete example with protected file download:
package main

import (
    "github.com/pocketbase/pocketbase"
    "github.com/pocketbase/pocketbase/core"
)

func main() {
    app := pocketbase.New()
    
    // Hook into file download to add custom logic
    app.OnFileDownloadRequest().BindFunc(func(e *core.FileDownloadRequestEvent) error {
        // Log file access
        app.Logger().Info("File download",
            "collection", e.Collection.Name,
            "record", e.Record.Id,
            "file", e.ServedName,
        )
        
        // Add custom header
        e.Response.Header().Set("X-Custom-Header", "value")
        
        // Continue with default serving
        return e.Next()
    })
    
    if err := app.Start(); err != nil {
        panic(err)
    }
}

Next steps

File upload

Learn how to upload files to records

S3 storage

Configure cloud storage for scalability