Project Documentation
Deployment, operations, and maintenance guidance for the StartPage project.
/docs serves FastAPI's OpenAPI UI (Swagger). This file (docs/index.html) is the static project guide used on the repository/GitHub Pages documentation site.
See FastAPI metadata docs: metadata for API.
StartPage is a self-hosted, frecency-based link manager designed to serve as your browser start page. Links are dynamically ranked using a weighted algorithm that combines frequency and recency of access.
/metricsAny tag input rendered with data-tag-input automatically bootstraps the suggestion widget defined in static/js/theme.js. Pass the existing tag names through data-tag-suggestions (JSON string) and the UI handles filtering, deduping, and insertion. This now powers the Add/Edit forms and the bulk tag field on /dashboard.
The /tags view now exposes an inline Rename control for every tag. Submitting the form posts to /tag/:id/rename and ultimately db_utils.rename_tag.
Admins can download backups and restore them from the dashboard banner. Exports stream via /exports/csv or /exports/json, while imports POST to /imports using the same schema.
id,name,url,rank,accessed,tags with semicolon-delimited tags.tags array.id blank to create a new link during import; provide an id to overwrite the name, URL, rank, accessed timestamp, and tags.uv run python -m services.io_utils csv -o backups/links.csv to export and curl -F format=csv -F upload=@links.csv http://host/imports to restore.The /fetch-title endpoint automatically extracts page titles from URLs to populate link names, improving user experience when adding bookmarks.
httpx library with 10-second timeout for HTTP requests.<title> tags (no BeautifulSoup dependency).None if title cannot be fetched.The /duplicates/check endpoint provides real-time validation to prevent duplicate links by checking both URL and name uniqueness.
StartPage supports URL parameters for creating embeddable widgets and forcing specific themes. These features are perfect for browser extensions, iframe integrations, or custom start page setups.
Add ?embed=true to any URL to enable embed mode. This hides the navigation bar and footer, creating a minimal widget view.
http://localhost:8080/?embed=true
http://localhost:8080/?embed=true&tag=work
http://localhost:8080/dashboard?embed=true
Force a specific theme using the ?theme parameter. Available options:
?theme=dark - Force dark mode (Catppuccin Mocha)?theme=light - Force light mode (Catppuccin Latte)http://localhost:8080/?theme=dark
http://localhost:8080/?theme=light
You can use both embed mode and theme parameters together. All navigation within embed mode (tag filters, search, pagination) automatically preserves these parameters.
http://localhost:8080/?embed=true&theme=dark
http://localhost:8080/?embed=true&theme=light&tag=personal
Create a new tab extension that shows your most-used links in a clean, minimal interface without navigation chrome.
Embed StartPage into dashboards, home automation panels, or other web applications with theme matching.
Display different tag-filtered views on different screens, each with its own theme preference.
Run StartPage on a dedicated display with a locked theme and minimal UI for public or shared workspaces.
The embed mode and theme parameters are:
embed_query_params handles parameter propagationprefers-color-scheme settings will see the forced theme instead of their system preference when the parameter is present.
Best for production deployments. Isolated environment with automatic restarts.
Good for development or personal deployments on a dedicated server.
just dockerbuild
This command builds the container and runs it with persistent storage at ~/.config/startpage
Build the Docker image:
docker build . -t startpage
Run the container:
docker run -d -p 8080:8080 \
--restart=always \
--name startpage \
-v ~/.config/startpage:/usr/startpage/data \
startpage
Environment variables you can set:
WORKERS - Number of Uvicorn workers (default: 2)LOG_LEVEL - Logging level (default: info)Once running, access StartPage at: http://localhost:8080
# Stop the container
docker stop startpage
# Start the container
docker start startpage
# View logs
docker logs startpage
# Restart the container
docker restart startpage
# or via justfile shortcut
just restart
# Remove the container
docker rm -f startpage
just docker-replace)To pull the latest code and replace the running container in one step:
just docker-replace
This recipe runs git pull, rebuilds the image, stops and removes the old container, and starts the new one with the same volume mount. Equivalent to running the stop/rm/build/run sequence manually.
Install just using your platform package manager: just.systems/man/en/packages.html
# Install dependencies
uv sync
# or
just install
Development mode (with hot reload):
just develop
# or with a custom port (default is 8000, not the Docker port 8080)
just develop 8080
# or directly
uv run uvicorn main:app --reload
Production mode (in screen session):
just run
This runs the app in a detached screen session named startpage. Use screen -r startpage to attach.
Standard run:
uv run uvicorn main:app
# Run all tests
uv run pytest
# Run specific test module
uv run pytest services/test_db_utils.py
# Type checking
uv run mypy services
data/links.db~/.config/startpage/links.db (mounted from host)For the full schema including an ER diagram and column descriptions, see dbschema.md in the project root.
For bulk edits or scripted maintenance you can work directly against the SQLite database.
sqlite3 data/links.db
.headers on
.mode column
SELECT id, name, rank FROM links ORDER BY rank DESC LIMIT 5;
UPDATE links SET rank = 1.0 WHERE name = 'Docs';
.quit
links (primary data), tags, and tagmap. Foreign keys keep orphaned rows from lingering, so deleting a tag automatically clears tagmap entries.
services.db_utils in your own scripts to re-use helpers such as save_link, get_links, rename_tag, or delete_link.
Remember: treat data/links.db as disposable state—commit schema changes and SQL scripts, not the database file.
| Version | Schema File | Features |
|---|---|---|
| 1.x | db_v1.sql | Basic links, rank, metadata tables |
| 2.x | db_v2.sql | + Tags and tagmap tables |
sqlite3 data/links.db "SELECT value FROM metadata WHERE name = 'db_version';"
StartPage automatically migrates your database when you upgrade to a new major version. The migration happens on the first application start after upgrading.
How it works:
metadata tablepyproject.tomlsql_scripts/db_version in the metadata tableVersion 2 adds the tagging feature. The migration adds two new tables:
tags - Stores tag definitionstagmap - Maps tags to links (many-to-many relationship)Steps to migrate:
# 1. Backup your database
cp data/links.db data/links.db.backup
# 2. Pull the latest code
git pull origin main
# 3. Install dependencies
uv sync
# 4. Start the application
# Migration will run automatically
uv run uvicorn main:app --reload
# 5. Verify migration succeeded
sqlite3 data/links.db "SELECT value FROM metadata WHERE name = 'db_version';"
# Should output: 2.0.0
If automatic migration fails, you can run the migration script manually:
# Backup first!
cp data/links.db data/links.db.backup
# Run migration script
sqlite3 data/links.db < sql_scripts/v1_to_v2.sql
# Verify
sqlite3 data/links.db "SELECT name FROM sqlite_master WHERE type='table';"
# Should show: links, metadata, tags, tagmap
If you need to roll back:
# Stop the application first
docker stop startpage
# or kill the uvicorn process
# Restore from backup
cp data/links.db.backup data/links.db
# Checkout previous version
git checkout v1.2.3
# Restart application
docker start startpage
Problem: Migration script fails with "table already exists"
Solution: Check if you're already on the target version. Run: SELECT value FROM metadata WHERE name = 'db_version';
Problem: Application won't start after migration
Solution: Check logs for errors. Verify database integrity: sqlite3 data/links.db "PRAGMA integrity_check;"
Local deployment:
cp data/links.db data/links.db.backup-$(date +%Y%m%d)
Docker deployment:
cp ~/.config/startpage/links.db ~/.config/startpage/links.db.backup-$(date +%Y%m%d)
Create a cron job for daily backups:
# Edit crontab
crontab -e
# Add this line (runs daily at 2 AM)
0 2 * * * cp ~/.config/startpage/links.db ~/.config/startpage/backups/links-$(date +\%Y\%m\%d).db
# Stop the application
docker stop startpage
# Restore the backup
cp data/links.db.backup-20250101 data/links.db
# Start the application
docker start startpage
Export to SQL format:
sqlite3 data/links.db .dump > startpage-export.sql
Export links to CSV:
sqlite3 -header -csv data/links.db "SELECT * FROM links;" > links.csv
StartPage exposes several important routes for end users and administrators. Understanding these helps with deployment, monitoring, and integration.
| Route | Purpose |
|---|---|
/ |
Homepage with frecency-ranked links and infinite scroll |
/dashboard |
Full link management with bulk operations (delete, tag) |
/tags |
Tag directory with rename, merge, and delete controls |
/stats |
Analytics dashboard showing rank distribution, top links, and pruning risks |
/settings |
Configuration page for batch size, max rank, temporary links, and navigation behavior |
/add |
Add new link with auto-fill, duplicate detection, and temporary link options |
/edit/{link_id} |
Edit existing link properties and tags |
/search |
HTMX-powered real-time search endpoint (query param: q) |
/help |
End user documentation and feature guide |
| Route | Method | Purpose |
|---|---|---|
/exports/csv |
GET | Export all links as CSV with headers: id,name,url,rank,accessed,tags |
/exports/json |
GET | Export all links as JSON array |
/imports |
POST | Bulk import links from CSV or JSON (form fields: format, upload) |
/fetch-title |
GET | Auto-fetch page title from URL for link name population |
/duplicates/check |
GET | Real-time duplicate detection (params: field=url|name, value) |
/redirect/{link_id} |
GET | Redirect to link URL and increment rank in background task |
/dashboard/bulk-delete |
POST | Delete multiple links by ID |
/dashboard/bulk-tag |
POST | Append tags to multiple links simultaneously |
| Route | Audience | Content |
|---|---|---|
/help |
End Users | Feature guide, daily usage tips, frecency explanation, and workflow documentation |
/docs |
Developers / API Consumers | FastAPI Swagger UI generated from the OpenAPI schema and API metadata |
/redoc |
Developers / API Consumers | Alternative ReDoc view of the same OpenAPI schema |
docs/index.html |
Project Maintainers | Static administrator guide intended for repository/GitHub Pages documentation |
Note: app route /docs is the FastAPI docs UI. This static page lives in docs/index.html for the project documentation site.
Settings are editable via the /settings screen (also linked in the app's navbar) and persisted to config.toml at the project root. Set STARTPAGE_CONFIG_PATH to point at a different writable TOML file if you want to store settings elsewhere.
| Setting | Default | Description |
|---|---|---|
frecency.batch_size |
20 | Number of links to load per page (pagination batch size) |
frecency.max_rank |
1000 | Total rank pool allowed before cleanup runs |
temp_links.enabled |
true | Whether temporary links can be created |
temp_links.default_ttl_hours |
24 | Duration applied when the "Default" preset is selected |
temp_links.max_custom_hours |
720 | Upper bound for the "Custom" preset (in hours, up to 30 days) |
temp_links.purge_interval_seconds |
600 | Background cleanup cadence in seconds (also runs on every read). The /settings UI displays and accepts this value in minutes. |
reset_filter_on_click |
false | When enabled, clicking a link clears the active tag filter and returns to the unfiltered home view. Stored in SQLite metadata (not config.toml); toggle via /settings. |
Prefer the UI whenever possible. For automation/headless workflows, edit config.toml with your favorite editor or provision a file before booting the app:
# config.toml
[frecency]
batch_size = 50
max_rank = 2000
[temp_links]
enabled = true
default_ttl_hours = 12
max_custom_hours = 72
purge_interval_seconds = 900
StartPage exposes a Prometheus-compatible endpoint at /metrics. Access is restricted by an IP/CIDR whitelist stored in the SQLite metadata table under metrics_whitelist.
Trusted proxy CIDRs used for forwarded /metrics headers are stored in metadata under trusted_proxy_cidrs.
Use the CLI helper to inspect and update the allow-list and trusted proxy entries:
# Local development usage
uv run python -m services.config_cli metrics-whitelist show
# Append a new CIDR/IP without replacing existing entries
uv run python -m services.config_cli metrics-whitelist add 10.0.0.0/8
# Remove an existing CIDR/IP entry
uv run python -m services.config_cli metrics-whitelist remove 10.0.0.0/8
# Replace allow-list entries
uv run python -m services.config_cli metrics-whitelist set 127.0.0.1/32 10.0.0.0/8
# Reset to local-only defaults (127.0.0.1 and ::1)
uv run python -m services.config_cli metrics-whitelist reset
# Clear all entries (blocks all /metrics clients)
uv run python -m services.config_cli metrics-whitelist clear
# Show trusted proxy CIDRs used for forwarded /metrics headers
uv run python -m services.config_cli trusted-proxies show
# Add your reverse proxy/container bridge source IP or CIDR
uv run python -m services.config_cli trusted-proxies add 172.17.0.1/32
# Docker shortcut (built into the image)
docker exec -it startpage startpage-config metrics-whitelist add 10.0.0.0/8
docker exec -it startpage startpage-config trusted-proxies add 172.17.0.1/32
When running behind nginx, pass the real client address explicitly so whitelist checks work correctly:
location / {
proxy_pass http://127.0.0.1:8001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
}
Forwarded headers are only trusted when the direct client IP matches the trusted proxy CIDR list (default: 127.0.0.1/32,::1/128).
If nginx runs from another host/container network, add that proxy source CIDR with the CLI (for example trusted-proxies add 172.18.0.0/16).
Trusted proxy CIDR updates apply immediately because /metrics reads these values from metadata for each request.
The OpenAPI docs at /docs (and /redoc) include the /metrics operation details and 403 behavior when the source IP is not whitelisted.
Replace job="startpage" if your Prometheus scrape job uses a different name.
# Scrape status
up{job="startpage"}
# Core gauges
startpage_links_total{job="startpage"}
startpage_temp_links_total{job="startpage"}
startpage_tags_total{job="startpage"}
startpage_rank_sum{job="startpage"}
# Temporary-link percentage
100 * startpage_temp_links_total{job="startpage"} / clamp_min(startpage_links_total{job="startpage"}, 1)
# /metrics traffic in last 24 hours
increase(startpage_metrics_requests_total{job="startpage"}[24h])
increase(startpage_metrics_denied_total{job="startpage"}[24h])
# Denied percentage in last 24 hours
100 * increase(startpage_metrics_denied_total{job="startpage"}[24h]) / clamp_min(increase(startpage_metrics_requests_total{job="startpage"}[24h]), 1)
# App uptime (hours)
startpage_metrics_uptime_seconds{job="startpage"} / 3600
# Whitelist entry count
startpage_metrics_whitelist_entries{job="startpage"}
# Counter resets in 7 days (process restart hint)
resets(startpage_metrics_requests_total{job="startpage"}[7d])
The ranking formula is defined in services/db_utils.py:
score = 10000 * rank * (3.75 / ((0.0001 * (current_time - last_accessed) + 1) + 0.25))
rank increments by 1 on each clickmax_rankMark a link as temporary from the add/edit form to set an automatic expiration (24 hours by default). Temporary links are highlighted in the UI and removed automatically when they expire.
Use /settings to toggle the feature, change the default duration, cap the custom duration, and adjust the cleanup interval. Updates are persisted to config.toml and apply immediately.
The /stats route provides a comprehensive analytics dashboard for monitoring link health and usage patterns. This is valuable for administrators who want to understand how the system is being used and identify potential issues before they occur.
Use Cases:
Check:
lsof -i :8080uv syncCheck integrity:
sqlite3 data/links.db "PRAGMA integrity_check;"
If corrupted: Restore from backup
Check:
sqlite3 data/links.db "SELECT COUNT(*) FROM links;"Check:
sqlite3 data/links.db ".tables"Solutions:
/settings or config.tomlsqlite3 data/links.db "VACUUM;"For additional support:
StartPage v2.7.0 - Project Administrator Guide
Last updated: February 2026