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...
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
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
// ...
}
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