PocketBase provides built-in file management capabilities with support for local and S3-compatible storage, automatic file validation, and thumbnail generation for images.
File fields
File storage is handled through file fields in your collections:
collection . Fields . Add ( & core . FileField {
Name : "attachments" ,
MaxSelect : 5 , // Allow up to 5 files
MaxSize : 5242880 , // 5MB per file
MimeTypes : [] string { // Restrict file types
"image/jpeg" ,
"image/png" ,
"application/pdf" ,
},
Thumbs : [] string { // Image thumbnails
"100x100" ,
"300x300" ,
"0x500" , // Width auto, height 500px
},
Protected : false , // Public access
Required : false ,
})
File field options
MaxSelect
Controls whether the field stores a single file or multiple files:
// Single file
field . MaxSelect = 1 // or 0
// Value type: string (filename)
// Multiple files
field . MaxSelect = 5
// Value type: []string (array of filenames)
MaxSize
Specifies the maximum file size in bytes:
field . MaxSize = 5 << 20 // 5MB
field . MaxSize = 10485760 // 10MB
If not set, the default maximum size is 5MB per file.
MimeTypes
Restricts allowed file types:
field . MimeTypes = [] string {
"image/jpeg" ,
"image/png" ,
"image/gif" ,
"image/webp" ,
"video/mp4" ,
"application/pdf" ,
"application/zip" ,
}
Leave empty to allow all file types.
Thumbs
Defines thumbnail sizes for image files:
field . Thumbs = [] string {
"100x100" , // Crop to 100x100 from center
"100x100t" , // Crop to 100x100 from top
"100x100b" , // Crop to 100x100 from bottom
"100x100f" , // Fit inside 100x100 (no cropping)
"0x300" , // Height 300px, width auto
"200x0" , // Width 200px, height auto
}
Thumbnails are generated automatically for supported image formats.
Protected
Controls file access:
// Public files (default)
field . Protected = false
// URL: /api/files/{collection}/{record}/{filename}
// Protected files
field . Protected = true
// URL: /api/files/{collection}/{record}/{filename}?token={fileToken}
Even public files have randomized filenames (e.g., document_Ab3Kf9g2.pdf), providing obscurity. Protected files require a short-lived token for access.
Uploading files
Single file upload
file := & filesystem . File {
Name : "document.pdf" ,
Reader : fileReader , // io.Reader
}
record . Set ( "attachment" , file )
app . Save ( record ) // File is uploaded during save
Multiple file upload
files := [] * filesystem . File {
{ Name : "image1.jpg" , Reader : reader1 },
{ Name : "image2.jpg" , Reader : reader2 },
{ Name : "image3.jpg" , Reader : reader3 },
}
record . Set ( "gallery" , files )
app . Save ( record )
Modifying uploaded files
// Get files pending upload
unsavedFiles := record . GetUnsavedFiles ( "documents" )
// Modify before saving
for _ , file := range unsavedFiles {
// Add prefix to filename
file . Name = "doc_" + file . Name
// Wrap reader for processing
file . Reader = newProcessingReader ( file . Reader )
}
app . Save ( record )
File modifiers
File fields support special modifier operations:
// Append files
record . Set ( "images+" , newFile )
record . Set ( "images+" , [] * filesystem . File { file1 , file2 })
// Prepend files
record . Set ( "+images" , newFile )
// Remove files by name
record . Set ( "images-" , "old_image.jpg" )
record . Set ( "images-" , [] string { "file1.jpg" , "file2.jpg" })
File storage
Storage structure
Files are stored in the following hierarchy:
storage/
└── {collectionId}/
└── {recordId}/
├── file1_Ab3Kf9g2.jpg
├── file2_Xk8Mn4p1.pdf
└── thumbs_file1_Ab3Kf9g2.jpg/
├── 100x100_file1_Ab3Kf9g2.jpg
└── 300x300_file1_Ab3Kf9g2.jpg
Filename normalization
PocketBase automatically adds a random suffix to filenames:
Original: document.pdf
Stored as: document_Ab3Kf9g2.pdf
This prevents:
Filename conflicts
Guessing file URLs
Accidental overwrites
Accessing files
File URLs
// Get filename
filename := record . GetString ( "avatar" )
// Build URL
url := fmt . Sprintf (
" %s /api/files/ %s / %s / %s " ,
app . Settings (). Meta . AppUrl ,
record . Collection (). Id ,
record . Id ,
filename ,
)
Public file URLs
For non-protected files:
GET /api/files/{collectionId}/{recordId}/{filename}
Example:
https://example.com/api/files/posts_collection/record123/image_Ab3Kf9g2.jpg
Protected file URLs
For protected files, generate a file token:
// Generate file token (valid for 3 minutes by default)
token , err := core . GenerateFileToken (
app ,
record ,
filename ,
)
// Build URL with token
url := fmt . Sprintf (
" %s /api/files/ %s / %s / %s ?token= %s " ,
app . Settings (). Meta . AppUrl ,
record . Collection (). Id ,
record . Id ,
filename ,
token ,
)
Thumbnail URLs
Access image thumbnails:
GET /api/files/{collectionId}/{recordId}/thumbs_{filename}/{size}_{filename}
Example:
https://example.com/api/files/posts/record123/thumbs_image_Ab3Kf9g2.jpg/100x100_image_Ab3Kf9g2.jpg
File validation
Automatic validation
File fields automatically validate:
File count (MaxSelect)
File size (MaxSize)
MIME type (MimeTypes)
Filename format
Custom validation
app . OnRecordValidate (). Bind ( & hook . Handler [ * core . RecordEvent ]{
Func : func ( e * core . RecordEvent ) error {
if e . Record . Collection (). Name == "posts" {
files := e . Record . GetUnsavedFiles ( "images" )
for _ , file := range files {
// Custom validation logic
if file . Size > 2 << 20 {
return validation . NewError (
"validation_file_too_large" ,
"Images must be under 2MB" ,
)
}
// Scan for malware, check dimensions, etc.
}
}
return e . Next ()
},
})
File operations
Listing files
// Get all filenames for a field
filenames := record . GetStringSlice ( "attachments" )
for _ , filename := range filenames {
fmt . Println ( filename )
}
Finding files
// Find which field contains a file
field := record . FindFileFieldByFile ( "document_Ab3Kf9g2.pdf" )
if field != nil {
fmt . Println ( "File is in field:" , field . Name )
}
Deleting files
Files are automatically deleted when:
Record is deleted
File field value is updated (old files removed)
File is explicitly removed using modifiers
// Remove specific file
record . Set ( "images-" , "old_image.jpg" )
app . Save ( record ) // File deleted after save
// Replace all files
record . Set ( "images" , newFiles )
app . Save ( record ) // Old files deleted
// Delete record
app . Delete ( record ) // All files deleted
S3 storage
PocketBase supports S3-compatible storage (AWS S3, MinIO, DigitalOcean Spaces, etc.):
app . Settings (). S3 = settings . S3Config {
Enabled : true ,
Bucket : "my-bucket" ,
Region : "us-east-1" ,
Endpoint : "s3.amazonaws.com" ,
AccessKey : "ACCESS_KEY" ,
Secret : "SECRET_KEY" ,
ForcePathStyle : false ,
}
app . SaveSettings ()
When switching from local to S3 storage, you must manually migrate existing files. New files will automatically use S3.
File hooks
Upload processing
app . OnRecordCreateExecute (). Bind ( & hook . Handler [ * core . RecordEvent ]{
Func : func ( e * core . RecordEvent ) error {
files := e . Record . GetUnsavedFiles ( "images" )
for _ , file := range files {
// Process image: resize, optimize, watermark, etc.
processedReader := processImage ( file . Reader )
file . Reader = processedReader
}
return e . Next ()
},
})
Post-upload actions
app . OnRecordAfterCreateSuccess (). Bind ( & hook . Handler [ * core . RecordEvent ]{
Func : func ( e * core . RecordEvent ) error {
// Files are now saved
filenames := e . Record . GetStringSlice ( "attachments" )
// Trigger async processing: virus scan, metadata extraction, etc.
for _ , filename := range filenames {
go processFileAsync ( e . App , e . Record , filename )
}
return e . Next ()
},
})
File deletion
app . OnRecordAfterDeleteSuccess (). Bind ( & hook . Handler [ * core . RecordEvent ]{
Func : func ( e * core . RecordEvent ) error {
// Files are already deleted by this point
// Use this for cleanup in external systems
app . Logger (). Info (
"Record files deleted" ,
"collection" , e . Record . Collection (). Name ,
"record" , e . Record . Id ,
)
return e . Next ()
},
})
Direct filesystem access
Getting filesystem instance
fsys , err := app . NewFilesystem ()
if err != nil {
return err
}
defer fsys . Close ()
File operations
// Upload file
path := record . BaseFilesPath () + "/" + filename
err := fsys . UploadFile ( file , path )
// Download file
reader , err := fsys . GetFile ( path )
// Delete file
err := fsys . Delete ( path )
// Delete directory
err := fsys . DeletePrefix ( record . BaseFilesPath ())
// Check if exists
exists , err := fsys . Exists ( path )
// Get file attributes
attrs , err := fsys . Attributes ( path )
fmt . Println ( "Size:" , attrs . Size )
fmt . Println ( "Modified:" , attrs . ModTime )
List files
// List files in directory
files , err := fsys . List ( record . BaseFilesPath ())
for _ , file := range files {
fmt . Println ( file . Key , file . Size )
}
Use appropriate file sizes
Set reasonable MaxSize limits. Smaller files upload faster and consume less storage. Consider compressing large files client-side before upload.
Only generate thumbnails you actually use. Each thumbnail size requires storage space and processing time.
Enable CDN for public files
Use a CDN in front of your PocketBase instance to cache and serve files closer to users.
Load images on-demand rather than all at once. Use smaller thumbnails for previews and full-size images only when needed.
S3-compatible storage is generally more reliable and scalable than local filesystem storage for production deployments.
Regularly check for and remove orphaned files or old temporary files to save storage space.
Security best practices
Validate file types strictly
Don’t rely solely on MIME types - they can be spoofed. Verify file contents match expected types.
PocketBase automatically sanitizes filenames, but be cautious when using user-provided filenames in other contexts.
Implement virus scanning for uploaded files, especially for user-facing applications.
Use protected files for sensitive data
Set Protected: true for files containing sensitive information. This requires authentication to access.
Limit file upload frequency and size per user to prevent abuse and resource exhaustion.
Set appropriate CORS headers