olaf bffd647a3e
Build LinkLog Development Image / development-image (push) Successful in 10s
Toolbar layout fixed
2026-09-05 08:45:39 +02:00
2026-09-05 08:31:23 +02:00
2026-09-05 08:45:39 +02:00
2026-09-05 08:45:39 +02:00
2026-08-24 14:30:30 +02:00
2026-08-25 09:07:29 +02:00
2026-08-28 11:14:11 +02:00
2026-09-05 08:05:44 +02:00
2026-08-27 17:33:43 +02:00
2026-08-25 08:39:46 +02:00
2026-08-28 08:19:43 +02:00
2026-08-25 08:39:46 +02:00
2026-08-24 22:54:01 +02:00
2026-08-26 10:30:33 +02:00
2026-09-05 08:05:44 +02:00
2026-08-24 14:30:30 +02:00
2026-08-26 20:54:00 +02:00

LinkLog

LinkLog is a Firefox extension and Python web service for saving links with a title, comment, timestamp, and tracking parameters removed. The service stores links in SQLite and can publish them through plugins, including Mastodon.

For full transparency: The author used vibe coding to create this software.

Project Layout

backend/       FastAPI application, services, database, and tests
frontend/      Jinja templates and browser-side assets
webextension/  Firefox Manifest V3 extension
Logo.svg       Source logo artwork used by the web and extension interfaces
Dockerfile     Backend container image
docker-compose.yml  Production app with Traefik reverse proxy hooks
docker-compose.local.yml  Local development app with direct port access
REQUIREMENTS.md    Product requirements
VIBE/           Conversation and prompt logs

Requirements

For local development:

  • macOS, Linux, or Windows with Python 3.11+
  • Firefox for installing the extension
  • Docker Desktop and Docker Compose v2 for the container workflow

The backend currently uses FastAPI, uvicorn, SQLite, and Pydantic. httpx2 is included for the Starlette-compatible test client. Jinja2 is included for server-rendered HTML templates. The backend version is 0.2.0 and is exposed through the FastAPI/OpenAPI metadata. It is read from frontend/version.json, the single source of truth shared by the backend and frontend. LinkLog is licensed under the GNU General Public License, version 3 or any later version. See LICENSE.

Local Installation

From the repository root:

python3.11 -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -r backend/requirements.txt

On Windows PowerShell, activate the environment with:

.venv\Scripts\Activate.ps1

The SQLite database is created automatically at backend/data/linklog.db when the application starts. On a fresh installation, open /setup and create the first administrator. No default user accounts are created by the application. The setup page also collects SMTP settings and requires a successful test email before creating the administrator.

The database schema is versioned with SQLite PRAGMA user_version. Application startup applies all pending migrations in order, so updating the application does not require deleting an existing database. New schema changes should be added as a new numbered migration in backend/app/database.py; existing migration entries must remain unchanged. The current schema, table reference, migration history, and Mermaid ERD are documented in DATABASE.md.

Run The Backend

Activate the virtual environment, then run uvicorn from the repository root:

. .venv/bin/activate
PYTHONPATH=. uvicorn backend.app.main:app --reload --host 127.0.0.1 --port 8000

Open these URLs:

The browser extension requires a backend URL to be entered during setup; it does not assume a default server.

The supplied Logo.svg is bundled as frontend/static/logo.svg for web pages and webextension/logo.svg for the Firefox popup and settings page. The visible LinkLog brand text uses the Google Foundry Asset font. Docker image builds download and bundle Asset, DM Sans, and Space Grotesk under /static/fonts, so the web frontend loads fonts from the LinkLog server rather than Google. The Firefox extension uses its bundled assets and local fallback fonts without requesting Google Fonts.

Regenerate Logo Assets

LinkLog.svg is the source logo. Install ImageMagick, then regenerate the web logo, extension logo, and Firefox toolbar icons with:

make logos

The generated files are frontend/static/logo.svg, webextension/logo.svg, and webextension/icon-16.png, icon-32.png, icon-48.png, and icon-96.png. Override the ImageMagick executable with make MAGICK=magick logos when needed.

Create an installable Firefox XPI bundle with:

make xpi

This creates XPI/unsigned/LinkLog-0.1.0.xpi from the webextension/ package and excludes macOS metadata and minified artifacts. The version is read from webextension/manifest.json. The XPI/signed/ directory is reserved for signed release bundles.

Docker Deployment

The main docker-compose.yml is the production deployment. It does not publish port 8000 on the host; the application is reachable through Traefik on the external linklog_traefik network. Set LINKLOG_PUBLIC_URL to the DNS hostname served by Traefik. The default is the documentation hostname linklog.example.com, which must be replaced for a real deployment.

For local development with direct access, use the separate file:

docker compose -f docker-compose.local.yml up --build

This publishes ${APP_PORT:-8000} and defaults the application URL to http://localhost:8000. Do not use the local file for an Internet-facing deployment.

Releases

Releases run in Gitea Actions when a v* tag is pushed. The Docker release version comes from frontend/version.json; the tag must match that version. The Firefox plugin version is independent and comes from the most recent signed XPI/signed/LinkLog-<version>.xpi checked into the repository.

The signed XPI is produced manually and should be checked into XPI/signed/LinkLog-<version>.xpi. Run make update-updates after adding a signed XPI to regenerate webextension/updates.json from every valid signed release artifact. The workflow validates the latest signed XPI's embedded manifest, publishes Docker images to git.kolkman.org/olaf/link-log:<backend-version> and :latest, and creates a release README that describes the project, the current backend/container version, and the raw signed XPI download URL with the plugin version.

The extension's update_url points at the stable raw repository URL https://git.kolkman.org/olaf/Link-Log/raw/branch/main/webextension/updates.json. Commit the regenerated webextension/updates.json together with each signed XPI. Release READMEs point to the raw signed XPI at https://git.kolkman.org/olaf/Link-Log/raw/branch/main/XPI/signed/LinkLog-<version>.xpi.

The workflow requires Gitea Actions secrets named REGISTRY_USERNAME, REGISTRY_TOKEN, and RELEASE_TOKEN. REGISTRY_TOKEN is a Gitea access token with permission to push packages; RELEASE_TOKEN needs permission to create releases and upload release assets.

Every push to main also runs .gitea/workflows/development.yml and publishes the current Docker image as git.kolkman.org/olaf/link-log:development. The workflow uses REGISTRY_USERNAME and the Gitea access token in REGISTRY_TOKEN, and can also be started manually from Gitea Actions.

Appending a username to the root URL, such as /alice, opens that user's public feed and profile information.

Configuration APIs require a bearer token returned by the login endpoint. Send it in the Authorization: Bearer ... header; query-string tokens are not accepted. Users authenticate with their email address; the username remains the public presentation identity used in profiles and feed URLs. User configuration uses the identity in that token. Plugin administration additionally requires an administrator account.

Login failures are throttled per client IP and email. Five failures within 15 minutes trigger a two-minute lockout, including invalid OTP attempts; successful authentication clears the failure counter.

Users can change their password from the profile page. The current password is required, new passwords must contain at least 8 characters, and the endpoint is PUT /api/user/password.

Users can configure a time-based one-time password from the profile page using an authenticator app. The profile displays a provisioning secret and authenticator URI during setup, then requires a current six-digit code to enable or disable OTP. When OTP is enabled, both the web login and Firefox extension settings login require the code. The TOTP secret is never returned by the profile API after setup.

Users can add up to five additional email addresses from the profile page. Each additional address must be validated through a verification email before it can be used for authentication. A verified alternative can be promoted to primary; the previous primary remains as a verified alternative. The profile displays validation status and offers resend controls subject to the same 20-second interval, five-send limit, and two-minute cooldown used during initial setup.

On the profile page, the authenticated username is displayed as read-only. Users can upload a PNG, JPEG, GIF, or WebP avatar up to 2 MB; uploaded files are stored in the persistent data volume and served by the application. Bio and email fields remain empty until the user provides values. Mastodon settings default to the mastodon.social instance and the From my #LinkLog: post prefix.

Run Tests

. .venv/bin/activate
PYTHONPATH=. pytest backend/tests -q

To ensure warnings are clean as well:

PYTHONPATH=. PYTHONWARNINGS=error pytest backend/tests -q

Install The Firefox Extension For Development

The extension is an unpacked Firefox extension. No build step is required. The toolbar uses square PNG icons generated from the bundled dark-background logo; logo.svg remains available for the popup and settings branding. The manifest includes stable Firefox extension metadata and references the packaged PNG icons for toolbar and add-on installation.

  1. Start the backend locally.
  2. Open Firefox and visit about:debugging#/runtime/this-firefox.
  3. Select Load Temporary Add-on.
  4. Choose webextension/manifest.json (Firefox 142 or newer is required).
  5. Open the LinkLog extension options and enter:
  • Backend URL: the URL of your LinkLog server, such as http://localhost:8000
  • Email: alice@example.com
  • Password: secret123
  • One-time password: enter it when OTP is enabled
  1. Save the settings and login.
  2. Open a webpage, select the LinkLog toolbar button, review the title and URL, add a comment, and submit it.

When the extension settings page has a valid session, it shows <username> logged in at <backend URL> and a Sign out button instead of the login form. Access and refresh credentials are kept in Firefox session storage, so a browser restart requires login again. Signing out revokes the token family and returns the form.

Temporary extensions are removed when Firefox restarts. Reload the extension from about:debugging after changing its files.

Docker

Create the Docker environment file before starting the stack:

cp .env.example .env

Edit .env and replace LINKLOG_SECRET_KEY with a long random value. Docker Compose automatically reads .env from the repository root. The committed .env.example contains safe defaults and placeholders; the real .env is ignored by Git.

When APP_ENV=production, application startup fails closed unless LINKLOG_SECRET_KEY is a non-default high-entropy value of at least 32 characters and LINKLOG_DATA_ENCRYPTION_KEY is a valid Fernet key. Development mode may use local defaults, but production secrets should come from a protected secret mechanism.

The main configurable values are:

Variable Purpose Default
LINKLOG_SECRET_KEY token signing/security secret required in Docker
LINKLOG_DATA_ENCRYPTION_KEY Fernet key for encrypting SMTP, Mastodon, and OTP secrets at rest required in Docker
LINKLOG_DATABASE_PATH SQLite file path inside the container /app/backend/data/linklog.db
LINKLOG_TOKEN_EXPIRY_MINUTES access-token lifetime 15
LINKLOG_REFRESH_TOKEN_EXPIRY_DAYS refresh-token lifetime 30
LINKLOG_PUBLIC_URL Public hostname used by Traefik and expanded to a callback URL by the backend linklog.example.com
LINKLOG_SMTP_HOST SMTP server hostname; empty disables delivery in local development empty
LINKLOG_SMTP_PORT SMTP server port 587
LINKLOG_SMTP_USERNAME SMTP login username empty
LINKLOG_SMTP_PASSWORD SMTP login password empty
LINKLOG_SMTP_FROM Sender address for verification mail LinkLog <no-reply@localhost>
LINKLOG_SMTP_USE_TLS Use STARTTLS for SMTP true
LINKLOG_EMAIL_VERIFICATION_EXPIRY_HOURS Verification-link lifetime 24
LINKLOG_PASSWORD_RESET_EXPIRY_HOURS Password-reset-link lifetime 1
LINKLOG_TRACKING_PARAMS comma-separated tracking parameters (stripped from logged URLs) built-in list
APP_PORT direct host port for FastAPI 8000

New users created by an administrator are email-unverified and cannot sign in until they follow the verification link sent to their address. The link is valid for LINKLOG_EMAIL_VERIFICATION_EXPIRY_HOURS hours and is handled by /api/auth/verify-email. Configure LINKLOG_SMTP_HOST, LINKLOG_SMTP_FROM, and the SMTP credentials for delivery; local development may leave the SMTP host empty, in which case accounts remain pending verification and no message is sent.

On a fresh installation, the setup form is prefilled from the LINKLOG_SMTP_* environment values when available. After saving, the values stored in the database are used for subsequent setup-page loads and mail delivery.

When a verified user enters the wrong password, LinkLog keeps the response generic and sends a password-reset link to that account's email address when SMTP is configured. Reset links expire after LINKLOG_PASSWORD_RESET_EXPIRY_HOURS hours, can be used once, and revoke existing sessions after the password is changed.

The full set of supported variables is listed in .env.example. Application variables are passed into the container by Compose; Docker and Traefik variables are used by Compose itself.

LINKLOG_DATA_ENCRYPTION_KEY must be a Fernet key kept outside the database. Generate one with a Python environment that has cryptography installed, for example python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())", then store it in .env or a protected deployment secret. Losing this key makes encrypted SMTP, Mastodon, and OTP values unrecoverable. Existing plaintext values from earlier versions should be rotated by saving them again after configuring the key.

Build and start the application:

docker compose up --build

The services are available at:

The SQLite database is stored in the named Docker volume linklog_data, mounted at /app/backend/data. The application runs as a non-root user and reports container health through /health;

Stop the stack without deleting its database:

docker compose down

Stop the stack and delete the named database volume:

docker compose down -v

For a real deployment, start with the docker-compose-example.yaml file. replace the router rule, configure TLS, protect the Traefik dashboard, avoid exposing the direct application port, and provide production secrets and authentication. The included Compose file is a local/prototype deployment scaffold, not a production security configuration.

Mastodon Configuration

After logging in, open http://localhost:8000/profile, enter the Mastodon instance, and select Connect Mastodon. LinkLog registers an OAuth application on that instance, opens Mastodon authorization, and stores the returned per-user access token after the callback. The requested scopes are read:accounts and write:statuses. Posts use the configured prefix followed directly by the title, an optional comment, a from: URL line when a title exists, and tags on the final line, with blank lines between sections.

  • Instance: hostname or URL such as mastodon.social or https://mastodon.social
  • Post prefix: text placed immediately before the link; defaults to From my #LinkLog:

LINKLOG_PUBLIC_URL is the public hostname used by Traefik and the backend. Use localhost for local development or a hostname such as linklog.kolkman.org for a deployment. The backend adds http:// for localhost and https:// for other hostnames when constructing OAuth and email links. Existing manually entered access tokens remain compatible with the plugin configuration API.

LinkLog caches the OAuth application credentials per Mastodon server in the persistent SQLite app_settings table, so subsequent connections do not register a new application on every attempt. If the server rate-limits application registration, the profile page reports the upstream 429 response and the user can retry after the server's cooldown.

Mastodon instances must be HTTPS hostname-only URLs that resolve to public IP addresses. Loopback, private, link-local, multicast, unspecified, reserved, and IPv4-mapped IPv6 destinations are rejected, and outbound redirects are refused.

Enable the plugin from the admin API or the admin page. New links are saved first and then posted to the configured instance at /api/v1/statuses. A Mastodon network failure does not undo the saved link.

Useful API Calls

Login:

curl -X POST http://localhost:8000/api/auth/login \\
  -H 'Content-Type: application/json' \\
  -d '{"email":"alice@example.com","password":"secret123"}'

The login response contains a 15-minute access token, a device-bound refresh token, its expiry time, and a device_id. Each successful refresh rotates the refresh token.

Refresh an access token:

curl -X POST http://localhost:8000/api/auth/refresh \\
  -H 'Content-Type: application/json' \\
  -d '{"refresh_token":"YOUR_REFRESH_TOKEN","device_id":"YOUR_DEVICE_ID"}'

Refresh-token reuse or a mismatched device ID returns 401 and revokes the token family. Signing out revokes the token family, while changing the password or completing a password reset revokes all sessions for the user.

Sign out with the access token in the bearer header:

curl -X POST http://localhost:8000/api/auth/logout \\
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'

Submit a link using the returned access token:

curl -X POST http://localhost:8000/api/links \\
  -H 'Content-Type: application/json' \\
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \\
  -d '{"title":"Example","url":"https://example.com","comment":"Worth reading"}'

The public feed is available at:

GET http://localhost:8000/api/public/feed

When a request includes a valid bearer token, entries owned by that authenticated user include edit permission and show an inline Edit action in the feed. The update endpoint is PUT /api/links/{link_id} and rejects edits from other users. Feed items also expose is_owner; it is true only for entries owned by the authenticated user and false for anonymous viewers or other users.

Links support zero to ten tags. Tags are trimmed, deduplicated case-insensitively, and retain their original casing for display. The Firefox capture popup shows existing server tags as checkboxes and accepts new comma-separated tags. The home page displays tags and provides a case-insensitive tag filter; editing a link replaces its complete tag set. The inline link editor also allows multiple existing tags to be selected and new tags to be entered. New tag values receive a leading # automatically, and the interface prevents saving more than ten tags. The installation seeds these available tags: #Internet, #Cybersecurity, #Fediverse, #Food, #Photography, #Music, and #AI. Users can create, edit, and delete their own labels from /labels. Administrators can delete any label from /admin; system-seeded labels have no user owner.

Administrators can enable one or more backend themes from /admin: Plain Day, Plain Night, Catppuccin Latte, Catppuccin Frappe, Catppuccin Macchiato, Catppuccin Mocha, Dracula, Nord, and Solarized. Visitors can choose among enabled themes; their choice is stored locally in the browser. At least one theme must remain enabled.

Admin plugin requests must include the administrator's token:

curl http://localhost:8000/api/admin/plugins \\
  -H 'Authorization: Bearer YOUR_ADMIN_ACCESS_TOKEN'

Administrators can manage user privileges from the admin page. The Administrator checkbox sends PUT /api/admin/users/{user_id} with {"is_admin": true} or {"is_admin": false}. The API rejects any change that would leave the system without an administrator.

Troubleshooting

  • ModuleNotFoundError: activate .venv and rerun python -m pip install -r backend/requirements.txt.
  • Jinja2 import error in Docker: rebuild the image with docker compose up --build; the runtime dependency is declared in backend/requirements.txt.
  • Extension cannot login: confirm the backend is running, use the exact backend origin without a trailing API path, and check the browser console for blocked requests.
  • Port 8000 is occupied: run uvicorn with another port and update the extension backend URL, for example --port 8001.
  • Port 80 or 8080 is occupied: change the host-side ports in docker-compose.yml.
  • Stale development data: stop the backend and remove backend/data/linklog.db, or run docker compose down -v for the container volume.
  • Schema migration issue: inspect the database version with sqlite3 backend/data/linklog.db 'PRAGMA user_version;' and restart the application to apply pending migrations.
S
Description
Vibe Coded Link Log
Readme
11 MiB
LinkLog 0.3.0
Latest
2026-09-06 11:04:29 +02:00
Languages
Python 57.6%
JavaScript 23.8%
HTML 10.5%
CSS 7.2%
Makefile 0.6%
Other 0.2%