Files
myworkspace/LinkSyncServer
DavidSaylor fe4cbc3537 feat: add web UI, query engine, session management, and 20 E2E tests
- Web UI: login, dashboard, links CRUD, collections, API keys, admin pages
- Query engine: AND/OR/XOR with field filters, tag search, preview endpoint
- Session management: token expiry detection, 401 interceptor, expiry banner
- Links search: tags included, multi-word AND, query mode with set operations
- Collections: static/dynamic, query builder with preview, public tree view
- Save as Collection: convert search results (static) or query (dynamic)
- Dashboard stats: resilient loading with allSettled pattern
- Login page: redesigned with public collections tree view
- Bug fix: query executor None fields crash (notes/description/url/title)
- E2E tests: 20 Playwright tests covering all critical user flows
- All 104 tests passing (84 unit/integration + 20 E2E)
2026-05-22 07:46:53 -05:00
..

LinkSyncServer

A self-hosted bookmark server with advanced collection and query capabilities, designed to work with browser extensions for bookmark synchronization.

Overview

LinkSyncServer replaces the need for workarounds in existing bookmark sync solutions. It provides:

  • True Collections - First-class collection objects with saved queries
  • Advanced Query Engine - Supports AND, OR, XOR set operations
  • Firefox-Compatible Fields - All bookmark attributes natively supported
  • Multi-User Support - Authentication with admin and regular user roles
  • RESTful API - Full CRUD operations for links and collections
  • Web Interface - Modern UI for browsing, searching, and managing collections
  • Docker-Ready - Easy deployment with Docker Compose

Features

Web Interface

The application includes a modern, responsive web interface with:

Authentication & Session Management

  • Login page with credential validation
  • Session expiry detection - automatically redirects to login when tokens expire
  • Proactive expiry warnings - shows countdown when session is about to expire (<2 minutes)
  • Graceful logout - clears local storage and redirects to login

Dashboard

  • Quick stats - displays total links, collections, and API keys (always shows numbers, never -)
  • Quick actions - one-click navigation to create links, collections, or API keys
  • Admin section - visible only to admin users for user management
  • Dual search modes:
    • Simple mode: Keyword search across title, URL, description, notes, and tags
    • Query mode: Full set operations with AND, OR, XOR, NOT, parentheses, and field filters
  • Multi-word search: Space-separated terms are AND-matched (all terms must be present)
  • Tag search: Tags are included in both simple and query searches
  • Save as Collection: Convert current search results or query into a collection:
    • Static: Saves the current result set as a fixed collection
    • Dynamic: Saves the query expression for live, auto-updating results
  • Full CRUD: Create, edit, and delete links with all Firefox bookmark fields

Collections Page

  • Collection cards - displays name, description, type, and visibility
  • Static collections: Manually managed link sets
  • Dynamic collections: Query-based with live preview:
    • Query builder UI - input field for query expressions
    • Preview button - shows matching links before saving
    • Result count - displays number of matches
    • Query expression display - shows saved query on collection cards
  • Public/Private toggle - controls visibility to other users

API Keys Page

  • Create API keys for browser extension sync
  • View active keys with creation dates
  • Delete keys to revoke access

Admin Page

  • User management - create, edit, delete users
  • Role assignment - admin or regular user roles
  • System statistics - overview of links, collections, users
  • Audit log - track all changes

Collections

Two types of collections:

Type Description
Static Explicit set of link IDs
Dynamic Query expression evaluated on each access

Dynamic Collection Query Syntax

('term1', 'term2', 'term3') OR tagA AND tagB XOR url:example.com
  • Parentheses evaluated first (innermost to outermost)
  • Left-to-right evaluation otherwise
  • Precedence: () > XOR > AND > OR

Set Operations

Query builder supports visual set operations:

Set1 AND Set2 XOR Set3 OR Set4

This evaluates as: (((Set1 AND Set2) XOR Set3) OR Set4)

Synchronization Modes

Mode Browser → Server Server → Browser
Bi-directional Add/update Add/update
Browser Authoritative Add/update Overwrite
Server Authoritative Download only Overwrite

Optional: Enable deletions for all modes.

All Firefox bookmark attributes supported:

  • id - Unique identifier
  • url - Bookmark URL (duplicates allowed)
  • title - Display title
  • description - Optional description
  • notes - User notes
  • tags - Array of tag names
  • favicon_url - Icon URL
  • path - Folder structure
  • created_at, updated_at - Timestamps
  • visit_count - Number of visits
  • is_bookmarked - Bookmarked status
  • source_set_id - Collection that added this link

Architecture

┌─────────────────────────────────────┐
│         LinkSyncServer              │
│                                     │
│  ┌──────────────┐  ┌─────────────┐ │
│  │   API Layer  │  │   Auth      │ │
│  └──────────────┘  └─────────────┘ │
│  ┌──────────────┐  ┌─────────────┐ │
│  │   Query      │  │   Models    │ │
│  │   Engine     │  │   (SQLAlchemy)│ │
│  └──────────────┘  └─────────────┘ │
│  ┌──────────────┐  ┌─────────────┐ │
│  │   Templates  │  │   Static    │ │
│  └──────────────┘  │   Files     │ │
│  ┌──────────────┐  └─────────────┘ │
│  │   PostgreSQL │                 │ │
│  │               │                 │ │
│  └──────────────┘                 │ │
└─────────────────────────────────────┘

Quick Start

Prerequisites

  • Docker and Docker Compose
  • Port 5000 available (or configurable)

Docker Compose

version: '3.8'

services:
  web:
    build: .
    ports:
      - "5000:5000"
    environment:
      - DATABASE_URL=postgresql://user:password@db:5432/linksync
      - SECRET_KEY=your-secret-key-here
      - ADMIN_USERNAME=admin
      - ADMIN_PASSWORD=admin123
    depends_on:
      - db

  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_DB=linksync
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
    volumes:
      - linkdata:/var/lib/postgresql/data

volumes:
  linkdata:

Build and Run

docker-compose up -d --build

How build: . Works

In docker-compose.yml, the web service uses build: . instead of image:. This is a key distinction:

Key Behavior
image: postgres:15-alpine Pulls a pre-built image from Docker Hub
build: . Builds a custom image from a Dockerfile in the current directory (.)

The build process works like this:

docker-compose up --build
        │
        ▼
  Reads docker-compose.yml
        │
        ▼
  Finds build: . → looks for Dockerfile in current directory
        │
        ▼
  Executes each instruction in the Dockerfile:
    1. FROM python:3.12-slim       ← Base image
    2. RUN apt-get install curl    ← Install system deps
    3. COPY requirements.txt .     ← Copy dependency list
    4. RUN pip install -r ...      ← Install Python packages
    5. COPY . .                    ← Copy all project files
    6. EXPOSE 5000                 ← Declare port
    7. CMD ["uvicorn", ...]        ← Set startup command
        │
        ▼
  Tags the built image as linksyncserver-web (auto-generated name)
        │
        ▼
  Starts the container from the built image

Why build instead of pull?

  • You're running your own application code, not a third-party image
  • Every code change requires a rebuild to take effect
  • The Dockerfile defines exactly how your app is packaged

Rebuilding after code changes:

# Rebuild and restart (picks up all code changes)
docker-compose up -d --build

# Rebuild without cache (forces fresh pip install)
docker-compose build --no-cache && docker-compose up -d

# Just restart without rebuilding (uses existing image)
docker-compose restart

The --build flag: Forces Docker Compose to rebuild images before starting containers. Without it, Compose reuses any previously built image, meaning your code changes won't be reflected.

Initial Login

  • URL: http://localhost:5000
  • Admin credentials from environment variables
  • Create first admin account
  • Admin can create regular users and admin users

API Documentation

See /api/docs or /api/openapi.json for complete API specification.

Configuration

Environment variables:

Variable Description Default
DATABASE_URL PostgreSQL connection string Required
SECRET_KEY JWT secret key Required
ADMIN_USERNAME Initial admin username -
ADMIN_PASSWORD Initial admin password -
DEBUG Debug mode False
HOST Bind address 0.0.0.0
PORT Port 5000

Project Structure

LinkSyncServer/
├── README.md
├── TODOs.txt
├── design.md
├── tasks.md
├── AGENTS.md
├── docker-compose.yml
├── Dockerfile
├── requirements.txt
├── app.py
├── config/
├── api/
├── models/
├── queries/
├── templates/
└── static/

Deployment

Deploy Script

The project includes deploy.ps1 (Windows) and deploy.sh (Linux/macOS) to prepare a clean deployment package. These scripts copy only production files, exclude development artifacts (tests/, __pycache__/, .git/, etc.), and create a starter .env file.

Usage

# Windows
.\deploy.ps1 C:\deploy\linksync
# Linux/macOS
chmod +x deploy.sh
./deploy.sh /opt/deploy/linksync

What Gets Deployed

linksync-deploy/
├── .env                 ← starter file (edit with production secrets)
├── .env.example
├── docker-compose.yml
├── Dockerfile
├── requirements.txt
├── app.py
├── api/
├── models/
├── queries/
├── config/
├── templates/
├── static/
├── alembic/
├── pyproject.toml
├── README.md
├── AGENTS.md
├── design.md
├── tasks.md
└── TODOs.txt

What Is Excluded

tests/, __pycache__/, .pytest_cache/, .git/, .vscode/, *.pyc, *.db, *.sqlite3, node_modules/, dist/, build/, and the deploy scripts themselves.

Full Deployment Workflow

# 1. Clone the repository to a temporary location
git clone <repo-url> /tmp/linksync-src
cd /tmp/linksync-src

# 2. Run the deploy script to prepare the package
./deploy.sh /opt/linksync

# 3. Configure production secrets
cd /opt/linksync
nano .env
# Set these values:
#   DATABASE_URL=postgresql://user:pass@db:5432/linksync
#   SECRET_KEY=<generate with: openssl rand -base64 32>
#   ADMIN_PASSWORD=<strong password>

# 4. Build and start
docker-compose up -d --build

# 5. Verify
curl http://localhost:5000/health

# 6. Clean up the source clone
rm -rf /tmp/linksync-src

Updating an Existing Deployment

# On the server, pull latest code and redeploy
cd /tmp/linksync-src && git pull
./deploy.sh /opt/linksync
cd /opt/linksync
docker-compose up -d --build
rm -rf /tmp/linksync-src

License

MIT License

Support

For issues and feature requests, see the GitHub repository.