v0.1.1 — 🌐 Try it live at linkbranch.duckdns.org | 📂 GitHub Repository
Link Branch is a self-hosted link-in-bio platform built with FastAPI, Jinja2, and SQLite. It supports custom profile design, shareable redirect links, deep analytics (including per-link insights), asset management, and full account management.
You can try the full application right now — no setup required:
Register a free account, build your page, and start sharing your links in minutes.
- Custom public profile page with Linktree-style island layout
- 404 page for missing profiles
- Auto
display_nameon registration - Section/divider links (
is_section) - Per-link 3-dot menu share/copy options
- Profile share sheet modal
- Dashboard "share my page" flow
- Analytics models:
ProfileViewLinkClickShareEvent
- Redirect route with logging:
GET /l/{link_id}— trackable public redirect (appears on your profile)GET /r/{redirect_id}— stealth redirect link (does NOT appear on your public profile page, but works in the background and is fully tracked in your analytics dashboard)
- Share logging endpoint:
POST /api/share
- Click logging endpoint:
POST /api/click
- Analytics dashboard:
GET /analytics- Two tabs: Overview (profile-button clicks) and Redirects (redirect-link clicks) — switch between them seamlessly
- KPIs: views, unique visitors, clicks, shares, CTR
- Trend charts (daily breakdown)
- Top links / platforms / referrers
- Device split (desktop / mobile / tablet / bot)
- Country & city aggregation (powered by queued ip-api.com lookups or Cloudflare/Vercel proxy headers as fallback)
- Recent clicks table (paginated)
- Per-link analytics drilldown (
GET /analytics/link/{link_id}):- Scoped KPIs: total clicks, unique clickers, CTR vs. profile views
- Daily click trend chart
- Device split for that specific link
- Top countries & cities for that link
- Top referrers for that link
- Recent click log (paginated)
- Date-range memory:
- Last selected analytics range is remembered in session
- Login/register with lowercase username enforcement
- Phone number support at registration
- Password policy validation (frontend checklist + backend enforcement)
- Forgot password flow (
/forgot-password) - My Profile page (
/my-profile)- Update profile details (name/email/phone)
- Change password (new password cannot equal old password)
- Permanent account delete flow
- Background customization:
- Solid color
- Gradient
- Image + overlay
- Island card customization:
- Frosted glass
- Solid color
- Gradient
- Image + overlay
- Avatar customization:
- URL source
- Shape
- Fit (
cover/contain) - Scale (70% to 140%)
- Button customization:
- Shape
- Fill style
- Button/text colors
- Hover effects
- Typography customization:
- Font family
- Font size
- Name/bio colors
- Branding toggle for public page footer
- Add/edit/delete links
- Add sections/dividers
- Enable/disable links
- Drag-and-drop reorder
- Icon picker (brand + custom uploaded icons)
- Upload/manage assets
- Copy trackable redirect link per dashboard link
Link Branch supports two types of redirect links:
| Type | Route | Visible on profile page? | Tracked in analytics? |
|---|---|---|---|
| Profile link | /l/{link_id} |
✅ Yes | ✅ Yes |
| Stealth redirect | /r/{redirect_id} |
❌ No | ✅ Yes |
Stealth redirect links (/r/...) are shareable URLs you can give out on other platforms (email campaigns, DMs, paid ads, etc.) without cluttering your public profile page. They log every click — including device, country, city, referrer — exactly like profile links do, but they never appear in your public link list. You manage them from the dashboard under the Redirects section.
From your analytics dashboard, click any link in the Top Links table to open its dedicated analytics page (/analytics/link/{link_id}). You'll see:
- Total clicks and unique clickers for that link
- CTR calculated against total profile views in the same period
- Daily click trend chart
- Device breakdown specific to that link
- Top countries and cities that clicked that link
- Top referrers driving traffic to that link
- Full paginated click log with timestamps, device, location, and referrer
Link Branch ships a storage.py module that automatically routes all file uploads to the right backend depending on your configuration. No code changes are needed to switch between modes.
| Condition | Upload destination | URL stored in DB |
|---|---|---|
| OCI env vars present | Oracle Cloud Object Storage bucket | https://objectstorage.…/o/<filename> |
| OCI env vars absent | static/uploads/ on local disk |
/static/uploads/<filename> |
The same logic applies to deletes — storage.py checks the stored URL at delete time so mixed-mode deployments (e.g. some old files on disk, new files in OCI) work correctly during migrations.
No extra steps. Uploaded assets are saved to static/uploads/ and served directly by the FastAPI StaticFiles mount. This is the default when no OCI variables are set.
- Create an OCI account at cloud.oracle.com (Always Free tier is sufficient).
- Create a bucket in Object Storage and set its visibility to Public.
- Generate an API key for your user under Identity → Users → API Keys. Download the
.pemprivate key file. - Note the fingerprint, user OCID, tenancy OCID, region, and namespace shown in the configuration preview.
scp -i "<your-ssh-key>" "<path-to-downloaded.pem>" user@<server-ip>:/path/to/link-branch/oci_private_key.pemLock down permissions:
chmod 600 /path/to/link-branch/oci_private_key.pemOCI_USER_OCID=ocid1.user.oc1..<your-user-ocid>
OCI_TENANCY_OCID=ocid1.tenancy.oc1..<your-tenancy-ocid>
OCI_FINGERPRINT=xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx
OCI_REGION=<region-identifier> # e.g. eu-amsterdam-1
OCI_NAMESPACE=<your-object-storage-namespace>
OCI_BUCKET_NAME=<your-bucket-name>
OCI_PRIVATE_KEY_PATH=/absolute/path/to/oci_private_key.pemsource venv/bin/activate
pip install ocipython3 -c "
import oci, os
from dotenv import load_dotenv
load_dotenv()
config = {
'user': os.getenv('OCI_USER_OCID'),
'fingerprint': os.getenv('OCI_FINGERPRINT'),
'tenancy': os.getenv('OCI_TENANCY_OCID'),
'region': os.getenv('OCI_REGION'),
'key_file': os.getenv('OCI_PRIVATE_KEY_PATH'),
}
client = oci.object_storage.ObjectStorageClient(config)
print('Connected! Namespace:', client.get_namespace().data)
"If this prints your namespace, restart the app and new uploads will go directly to your bucket.
Existing files in static/uploads/ are not automatically moved. Their /static/uploads/... URLs will continue to work as long as the directory exists on disk. To migrate them, upload each file to the bucket manually (or write a one-time migration script) and update the url column in the assets table to the OCI URL.
- FastAPI
- Uvicorn
- SQLAlchemy
- Jinja2 templates
- SQLite (default)
- bcrypt password hashing
- Session auth via Starlette
SessionMiddleware - ip-api.com for server-side geo analytics; bounded queue + cache included, falls back to Cloudflare/Vercel proxy headers
- Oracle Cloud Object Storage via
ociPython SDK (optional; falls back to local disk)
main.py: app bootstrap, router registration, migration helpers, sitemap + robots.txtmodels.py: SQLAlchemy models (User,Link,Asset,RedirectLink,ProfileView,LinkClick,ShareEvent)database.py: DB engine/session setupgeo.py: IP geolocation helper (ip-api.com + bounded queue/cache + proxy header fallback)security.py: rate limiting, input sanitisation, client IP resolutionstorage.py: dual-mode asset storage — routes uploads/deletes to OCI or local disk automaticallyroutes/:auth.pydashboard.pypublic.py— public profile rendering,/l/{link_id},/r/{redirect_id}settings.pyanalytics.py— main analytics page + per-link drilldownprofile.pyhome.pyadmin.py
templates/: Jinja2 templates (dashboard, profile, settings, analytics, link_analytics, auth, home, etc.)static/uploads/: uploaded user assets (used when OCI is not configured)
- Clone the repository:
git clone https://github.com/MDnoob/Link-Branch.git cd Link-Branch - Create and activate a virtual environment:
python -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate
- Install dependencies:
pip install -r requirements.txt
- (Optional) Install the OCI SDK if you want cloud asset storage:
pip install oci
- Create a
.envfile:SECRET_KEY=<your-secret-key> # Optional: restrict CORS origins CORS_ALLOW_ORIGINS=https://yourdomain.com # Optional: set the public base URL (used in sitemap + share links) PUBLIC_BASE_URL=https://yourdomain.com # Optional: grant super-admin access SUPERADMIN_USERNAME=<your-username> # Optional: enforce secure cookies in production SESSION_HTTPS_ONLY=true ENV=production ENABLE_DOCS=false # Optional: custom database path DATABASE_URL=sqlite:///./branchtree.db # Optional: backend external-service queue limits GEO_LOOKUP_CONCURRENCY=4 GEO_LOOKUP_QUEUE_TIMEOUT_SECONDS=2 GEO_LOOKUP_TIMEOUT_SECONDS=3 GEO_CACHE_TTL_SECONDS=86400 OCI_STORAGE_CONCURRENCY=4 OCI_STORAGE_QUEUE_TIMEOUT_SECONDS=15 # Optional: Oracle Cloud Object Storage (leave blank to use local disk) OCI_USER_OCID= OCI_TENANCY_OCID= OCI_FINGERPRINT= OCI_REGION= OCI_NAMESPACE= OCI_BUCKET_NAME= OCI_PRIVATE_KEY_PATH=
- Run the development server:
uvicorn main:app --reload
- Open in your browser:
http://127.0.0.1:8000
For production, do not use --reload. Set ENV=production, configure a real SECRET_KEY, and run under your process manager with a command like:
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 2GET /loginPOST /loginGET /registerPOST /registerGET /forgot-passwordPOST /forgot-passwordGET /logout
GET /dashboardPOST /dashboard/links/addPOST /dashboard/links/add-sectionPOST /dashboard/links/{link_id}/editPOST /dashboard/links/{link_id}/togglePOST /dashboard/links/{link_id}/deletePOST /dashboard/links/reorderGET /assetsPOST /dashboard/assets/uploadPOST /dashboard/assets/{asset_id}/delete
GET /settingsPOST /settingsGET /my-profilePOST /my-profilePOST /my-profile/passwordPOST /my-profile/deleteGET /profile/{username}— public profile pageGET /l/{link_id}— tracked profile-link redirectGET /r/{redirect_id}— stealth redirect (hidden from public profile)
POST /api/sharePOST /api/clickPOST /api/frontend-errorGET /analytics— main analytics dashboard (Overview + Redirects tabs)GET /analytics/link/{link_id}— per-link analytics drilldown
GET /healthGET /robots.txtGET /sitemap.xml
GET /admin(accessible only to usernames listed inSUPERADMIN_USERNAMEorSUPERADMIN_USERNAMES)
- Geo analytics (country/city) can be inconsistent in local development environments. This is expected because local/private IPs are skipped and proxy headers are only present on deployments behind Cloudflare or Vercel.
- Current release:
0.1.1 - Live demo: linkbranch.duckdns.org
- Repository: github.com/MDnoob/Link-Branch