Overview
Sptfy.in uses PocketBase as its backend database. PocketBase is a self-hosted SQLite database with a built-in admin UI and REST API. This guide covers:- Creating the admin account
- Importing the database schema
- Understanding the collections structure
- Running migrations
Initial Setup
Create Admin Account
After starting PocketBase for the first time, create an admin account:Access the admin panel
Open your browser and navigate to:Local development:Production (after setting up Nginx reverse proxy):
If running in Docker on a VPS, you may need to SSH tunnel to access the admin panel:Then open
http://localhost:8090/_/ in your browser.Create admin credentials
On first access, you’ll see a setup screen:
- Email: admin@yourdomain.com (use a real email)
- Password: Choose a strong password (16+ characters, use a password manager)
- Confirm Password: Re-enter your password
Import Database Schema
The repository includes migration files that define the database schema. You have two options:Option 1: Automatic Migration (Recommended)
Migrations inpocketbase/pb_migrations/ run automatically when the container starts.
If you see errors about duplicate collections, migrations may have already run. This is normal on restarts.
Option 2: Manual Schema Import
If you need to manually import the schema (e.g., from production to local dev):Export schema from production
- Log in to production PocketBase admin:
https://pbbase.sptfy.in/_/ - Go to Settings → Export collections
- Download the JSON file (e.g.,
pb_schema_export.json)
Database Structure
Collections Overview
Sptfy.in uses the following collections:users - User accounts
users - User accounts
Stores user authentication and profile data.Key fields:
id(text) - Unique user IDemail(email) - User emailemailVisibility(bool) - Whether email is publicverified(bool) - Email verification statusname(text) - Display nameavatar(file) - Profile picture
- Spotify OAuth (primary authentication method)
- List:
@request.auth.id != ""(authenticated users only) - View:
@request.auth.id != ""(authenticated users only) - Create: Public (via Spotify OAuth)
- Update:
@request.auth.id = id(users can update their own profile) - Delete: Admin only
random_short - Short links
random_short - Short links
Stores all shortened Spotify links (both random and custom slugs).Key fields:
id(text) - Unique link IDslug(text) - Short URL slug (e.g., “abc123” or “my-playlist”)destination(url) - Original Spotify URLuser(relation) - User who created the link (optional, for authenticated users)created(date) - Timestamp of creationclicks(number) - Total click countlast_clicked(date) - Last click timestamp
- Unique index on
slug(prevents duplicate slugs)
- List:
@request.auth.id != "" && user = @request.auth.id(users see their own links) - View: Public (anyone can view link details)
- Create: Public with Turnstile validation
- Update:
@request.auth.id = user(owner can update) - Delete:
@request.auth.id = user(owner can delete)
analytics - Click tracking
analytics - Click tracking
Tracks individual clicks on short links for analytics.Key fields:
id(text) - Unique analytics record IDlink(relation) - Related short linktimestamp(date) - Click timestampip(text) - IP address (hashed for privacy)user_agent(text) - Browser user agentreferrer(url) - Referrer URLcountry_code(text) - 2-letter country code (from IP geolocation)
- IP addresses are hashed before storage
- Country code determined via ip-api.com (see
pb_hooks/random.pb.js)
- List:
@request.auth.id != "" && link.user = @request.auth.id(owner sees analytics) - View:
@request.auth.id != "" && link.user = @request.auth.id - Create: Internal only (via hooks)
- Update: Admin only
- Delete: Admin only
blocks - Blocked users/IPs
blocks - Blocked users/IPs
Stores blocked users or IP addresses to prevent abuse.Key fields:
id(text) - Unique block IDtype(select) - Block type:useroripvalue(text) - User ID or IP addressreason(text) - Block reasoncreated(date) - Block timestampexpires(date) - Expiration date (optional)
- List: Admin only
- View: Admin only
- Create: Admin only
- Update: Admin only
- Delete: Admin only
viewList - View analytics aggregation
viewList - View analytics aggregation
View collection that aggregates analytics data for dashboard display.Purpose: Pre-computed analytics summaries to improve dashboard performance.Fields: Aggregates clicks by time period, country, referrer, etc.API Rules:
- List:
@request.auth.id != ""(authenticated users only) - View:
@request.auth.id != "" - Create/Update/Delete: Not applicable (view-only)
PocketBase Hooks
Hooks are custom JavaScript functions that run on specific events (e.g., record creation). Location:pocketbase/pb_hooks/
Available Hooks
Hooks are loaded automatically when PocketBase starts. Changes to hook files require a container restart:
Migrations
Migrations define schema changes over time. They’re stored inpocketbase/pb_migrations/.
How Migrations Work
-
Each migration is a JavaScript file with a timestamp name:
-
PocketBase tracks which migrations have run in
pb_data/data.db - On startup, PocketBase runs any new migrations in order
Creating New Migrations
Don’t manually create migration files. Instead:- Make schema changes in the PocketBase admin UI
- PocketBase automatically generates migration files
- Commit the new migration files to git
- Deploy (GitHub Actions will pull the new migrations)
Local Development Setup
For local development, you need to run PocketBase locally:Windows
- Sets
CF_SECRET_KEYto the Turnstile test key (always passes) - Starts PocketBase on
http://127.0.0.1:8090
Linux/macOS
Connecting Frontend to Local Backend
Update your frontend.env file:
The test Turnstile site key
1x0000000000000000000000000000000AA always passes validation. Perfect for local development.Database Backups
Automated Backups (Production)
Production uses PocketBase’s built-in S3 backup to Cloudflare R2. Configure in PocketBase Admin:- Settings → Backups
- Enable S3 backups
- Set schedule (e.g., daily at 3 AM)
Manual Backups
Restore from Backup
Troubleshooting
Migrations fail to run
Error:migration already applied
Solution: This is normal on restarts. PocketBase tracks applied migrations and skips them.
Cannot access admin panel
Error: Connection refused athttp://localhost:8090
Solution: Check if PocketBase is running:
“Collection not found” errors
Cause: Migrations haven’t run or schema import failed. Solution: Check logs for migration errors:Database locked errors
Cause: SQLite can have lock contention under high load. Solution: Increase VPS resources or optimize queries. For very high traffic, consider migrating to PostgreSQL (requires forking PocketBase or using a different backend).Next Steps
- Configuration - Set up environment variables and settings
- Deployment - Deploy to production with Nginx and Cloudflare Pages