# Transmute > Transmute is a free, open-source, self-hosted file converter and compressor. Convert and compress images, video, audio, data, documents, 3D models, and more on your own hardware with Docker. No file size limits, no watermarks, no third-party uploads — full privacy. Transmute is a self-hosted alternative to cloud-based file converters like CloudConvert, FreeConvert, and Convertio. It runs entirely on your own hardware via Docker, keeping all files private. It supports converting between hundreds of formats across images (PNG, JPG, WebP, SVG, BMP, HEIC), video (MKV, MP4, MOV, AVI, WebM), audio (MP3, WAV, FLAC, AAC, OGG), documents (PDF, DOCX, TXT, Markdown, HTML), data (CSV, JSON, YAML, Parquet), 3D models (STL, OBJ, PLY, GLB, 3MF, DXF), diagrams (draw.io), fonts, subtitles, and more. It also compresses images, PDFs, audio, video, and GIFs to reduce file size while keeping the same format, with simple compression levels to balance quality and size. It has a clean web UI, a full REST API, built-in authentication with per-user data isolation, and seven built-in themes. - License: MIT - Source Code: https://github.com/transmute-app/transmute - Docker Image: ghcr.io/transmute-app/transmute:main --- # Getting Started Transmute is a self-hosted file converter. Follow the steps below to get up and running. ## Prerequisites - [Docker](https://docs.docker.com/get-docker/) and Docker Compose installed on your machine. ## Quick Start > **Warning:** Think carefully before exposing Transmute to the public internet. Transmute includes built-in [authentication and per-user data isolation](/docs/authentication), but is designed for trusted networks. If you expose it beyond your LAN, place it behind a reverse proxy with TLS and rate limiting. The maintainers are not responsible for security issues arising from your deployment configuration. Download the compose file and and start the stack with a single command: ```bash wget "https://raw.githubusercontent.com/transmute-app/transmute/refs/heads/main/docker-compose.yml" && docker compose up -d ``` Then open [http://localhost:3313](http://localhost:3313) in your browser. ## Verifying the Installation Run the following command to check all services are healthy: ```bash docker compose ps ``` You should see output similar to: ```bash NAME STATUS transmute running (healthy) ``` > **Tip:** If a container shows `unhealthy`, check its logs with `docker compose logs `. # Kubernetes For those who wish to host Transmute on a Kubernetes cluster, an example configuration is available. You can download it to a local folder with the command: ```bash wget "https://raw.githubusercontent.com/transmute-app/transmute/refs/heads/main/k8s-deployment.yml" ``` You cannot, however, use this file immediately as Kubernetes clusters vary widely in configuration. Use the comments in the downloaded file to customize the deployment to match your cluster's storage back-end and ingress provider. Once this is done, you can deploy Transmute to your cluster with a simple: ```bash kubectl apply -f k8s-deployment.yaml ``` --- # Authentication & Users Transmute requires every user to authenticate before uploading, converting, or downloading files. Each user's data (uploads, conversions, settings, default formats) is fully isolated — you can only see and manage your own files. --- ## First-Time Setup When Transmute starts with an empty database, it enters **bootstrap mode**. The first screen you see is a **Create Admin** form instead of the usual login page. 1. Choose a **username** and **password** (minimum 8 characters). 2. Optionally fill in an **email** and **full name**. 3. Click **Create Admin**. This account becomes the initial administrator. Once created, bootstrap mode is permanently disabled and all future visitors see the standard login page. > **Tip:** If you're deploying with Docker and want to script the initial setup, you can `POST` to `/api/users` with a JSON body — the first user created is always promoted to admin regardless of the `role` field. --- ## Roles Transmute has two roles: | Role | Capabilities | |------|-------------| | **Admin** | Full access — manage all users, configure cleanup settings, and access everything a member can | | **Member** | Upload, convert, and download their own files; manage their own account, settings, default formats, and API keys | Key differences: - Only admins can **create, edit, disable, or delete** other users. - Only admins can change **cleanup TTL** and **cleanup interval** settings (under Settings → Data Management). - Admins **cannot demote themselves** or **delete their own account** — another admin must do it. - Members cannot see or access other users' files or conversion history. --- ## Logging In Navigate to your Transmute instance in a browser. Enter your **username** and **password**, then click **Log In**. A session token (JWT) is issued and stored in your browser — you stay logged in until the token expires (default: 60 minutes) or you log out. --- ## Managing Your Account Click your **username** in the header to open the **Account** page. From here you can: - Update your **username**, **email**, or **full name** - Change your **password** (minimum 8 characters) - Manage your **API keys** (see below) Changes take effect immediately after clicking **Save Changes**. --- ## Managing Users (Admin) Admins can access the **Users** page from the navigation bar. From this page you can: ### Create a User 1. Click **Create User**. 2. Fill in the username, password, and optionally email and full name. 3. Choose a **role** (admin or member). 4. Click **Create**. ### Edit a User Click the **edit** button on any user card to change their username, email, full name, password, or role. You cannot edit your own role or disabled status from the Users page — use the Account page for self-service changes. ### Disable a User Toggle the **Disabled** switch on a user card. Disabled users cannot log in or use API keys, but their data is preserved. Re-enable them at any time. ### Delete a User Click the **delete** button on a user card. This permanently removes the user and **cascade-deletes all of their data** — uploads, conversions, conversion history, settings, default formats, and API keys. This action cannot be undone. > **Warning:** You cannot delete your own admin account. If you need to remove yourself, another admin must do it. --- ## API Keys API keys let you authenticate with the Transmute API without using a username and password. They're ideal for scripts, CI pipelines, and other automated workflows. ### Creating an API Key 1. Go to **Account** → **API Keys**. 2. Enter a descriptive **name** (e.g. "CI pipeline" or "Backup script"). 3. Click **Create**. 4. **Copy the key immediately** — it is shown only once and cannot be retrieved later. Each user can have up to **25 API keys**. ### Using an API Key Pass the key as a Bearer token in the `Authorization` header, exactly as you would with a JWT: ```bash curl -H "Authorization: Bearer YOUR_API_KEY" \ http://localhost:3313/api/files ``` API keys have the same permissions as the user who created them. If the user account is disabled, all of its API keys stop working immediately. ### Deleting an API Key Click the **×** button next to any key on the Account page. The key is revoked immediately. --- ## Using Authentication with the API All API endpoints (except health checks and bootstrap status) require a valid Bearer token. You can obtain one in two ways: ### Option 1 — Username & Password (JWT) ```bash curl -X POST http://localhost:3313/api/users/authenticate \ -H "Content-Type: application/json" \ -d '{ "username": "alice", "password": "correct horse battery staple" }' ``` **Response:** ```json { "access_token": "eyJhbGciOiJIUzI1NiIs...", "token_type": "bearer", "expires_in": 3600, "user": { "uuid": "...", "username": "alice", "role": "member", "disabled": false } } ``` Use the `access_token` value in subsequent requests: ```bash curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \ http://localhost:3313/api/files ``` JWTs expire after the configured lifetime (default: 60 minutes). Request a new token when the current one expires. > **Tip:** The OAuth2 token endpoint `POST /api/users/token` is also available if your HTTP client supports the standard `application/x-www-form-urlencoded` OAuth2 password flow. ### Option 2 — API Key Generate an API key from the Account page (or via `POST /api/api-keys`) and pass it directly as a Bearer token: ```bash curl -H "Authorization: Bearer tm_abc123..." \ http://localhost:3313/api/files ``` API keys do not expire on their own — they remain valid until deleted or the owning user is disabled. --- ## Quick Reference | Method | Endpoint | Auth | Description | |--------|----------|------|-------------| | `GET` | `/api/users/bootstrap-status` | None | Check if first-time setup is needed | | `POST` | `/api/users` | None (bootstrap) / Admin | Create a user | | `POST` | `/api/users/authenticate` | None | Log in with username & password | | `POST` | `/api/users/token` | None | OAuth2 password-flow token endpoint | | `GET` | `/api/users/me` | User | Get the authenticated user | | `PATCH` | `/api/users/me` | User | Update your own account | | `GET` | `/api/users` | Admin | List all users | | `GET` | `/api/users/{uuid}` | Admin | Get a user by UUID | | `PATCH` | `/api/users/{uuid}` | Admin | Update a user | | `DELETE` | `/api/users/{uuid}` | Admin | Delete a user and all their data | | `GET` | `/api/api-keys` | User | List your API keys | | `POST` | `/api/api-keys` | User | Create an API key | | `DELETE` | `/api/api-keys/{id}` | User | Delete an API key | --- OIDC support allows users to sign in to Transmute using an external identity provider such as Authentik, Authelia, or another OpenID Connect compatible SSO solution. If you are using Transmute with OIDC, and your Transmute instance is behind a reverse proxy, please make sure to set your `APP_URL` evironment variable as well. See [environment variables](/docs/environment-variables/). # Configuration OIDC is configured through the following environment variables: ```bash # Examples for Authentik OIDC_ISSUER_URL=http:///application/o/transmute/ OIDC_INTERNAL_URL=http:///application/o/transmute/ OIDC_CLIENT_ID= OIDC_CLIENT_SECRET= OIDC_DISPLAY_NAME=Authentik OIDC_USERNAME_CLAIM=custom_username OIDC_AUTO_CREATE_USERS=true OIDC_AUTO_LAUNCH=true ``` If your provider requires you to set a "callback" or "redirect" URI, the value should be `/api/oidc/callback`. Examples: - `https://transmute.domain.com/api/oidc/callback` - `http://192.168.1.1:3313/api/oidc/callback` Again - if you are using Transmute with OIDC, and your Transmute instance is behind a reverse proxy, please make sure to set your `APP_URL` evironment variable as well. See [environment variables](/docs/environment-variables/). ## Environment Variables ### OIDC\_ISSUER\_URL The public issuer URL for your OIDC provider. This should be the URL Transmute uses to redirect users to your identity provider in the browser. In most setups, this is the externally accessible URL. Example: ```bash OIDC_ISSUER_URL=https://auth.domain.com/application/o/transmute/ ``` ### OIDC\_INTERNAL\_URL The internal URL Transmute uses to communicate with the OIDC provider from inside your Docker network or local environment. This is useful when the public URL is not reachable from the Transmute container, or when you want to avoid unnecessary external routing. If not set, this defaults to `OIDC_ISSUER_URL`. Example: ```bash OIDC_INTERNAL_URL=http://192.168.1.1:9000/application/o/transmute/ ``` ### OIDC\_CLIENT\_ID The client ID for your OIDC application. ### OIDC\_CLIENT\_SECRET The client secret for your OIDC application. ### OIDC\_DISPLAY\_NAME The label shown on the login button in the Transmute UI. Defaults to `SSO`. Example: ```bash OIDC_DISPLAY_NAME=Authentik ``` ![OIDC_DISPLAY_NAME set to "Authentik"](/screenshots/login.png) ### OIDC\_USERNAME\_CLAIM A custom username claim to use instead of the default `preferred_username`. This is useful if your provider uses a different claim for the username, or if you want to use a custom claim that contains the username in a specific format. See issue [#164](https://github.com/transmute-app/transmute/issues/164) for more details. ### OIDC\_AUTO\_CREATE\_USERS Controls whether users are automatically created in Transmute the first time they successfully sign in through OIDC. * `true`: users are created automatically on first login * `false`: users must already exist in Transmute before they can log in with OIDC Defaults to `true`. ### OIDC\_AUTO\_LAUNCH Controls whether or not `/auth` automatically triggers the OAUTH flow, or if users should have to click "Log in with SSO" first. Setting this to true means you cannot use username and password to log in through the UI. # URL Example A common setup uses a public URL for browser redirects and an internal URL for container-to-container communication: ```bash OIDC_ISSUER_URL=https://auth.domain.com/application/o/transmute/ OIDC_INTERNAL_URL=http://192.168.1.1:9000/application/o/transmute/ ``` # Docker Compose Example Below is a working example for integrating Transmute with Authentik: ```yaml services: transmute: image: ghcr.io/transmute-app/transmute:latest container_name: transmute restart: unless-stopped ports: - 3313:3313 volumes: - transmute_data:/app/data environment: - OIDC_ISSUER_URL=http://:9000/application/o/transmute/ - OIDC_CLIENT_ID= - OIDC_CLIENT_SECRET= - OIDC_DISPLAY_NAME=Authentik healthcheck: test: - CMD - wget - -q - -O - /dev/null - --tries=1 - http://localhost:3313/api/health/ready interval: 30s timeout: 10s retries: 3 start_period: 40s volumes: transmute_data: ``` # Notes * Your OIDC provider must be configured with the correct redirect URL for Transmute. * `OIDC_ISSUER_URL` should usually be the user-facing URL that browsers can access. * `OIDC_INTERNAL_URL` is only needed when Transmute cannot reliably reach the provider through the public URL. * Providers such as Authentik and Authelia should work as long as they expose a standard OpenID Connect issuer. If a specific provider does not work, please [open an issue](https://github.com/transmute-app/transmute/issues). --- # Themes Transmute ships with several built-in themes. Choose between dark and light variants to suit your preference. ## Custom Themes Transmute also supports custom themes. As an admin, you can create your own theme in the settings panel. Custom themes allow you to personalize the look and feel of your Transmute instance. Themes created by admins can be chosen by all users, but only admins have the ability to create and manage them. --- ## Built-In Dark Themes ### Rubedo (Default) ![Rubedo theme](/screenshots/rubedo.png) ### Citrinitas ![Citrinitas theme](/screenshots/citrinitas.png) ### Viriditas ![Viriditas theme](/screenshots/viriditas.png) ### Nigredo ![Nigredo theme](/screenshots/nigredo.png) --- ## Built-In Light Themes ### Albedo ![Albedo theme](/screenshots/albedo.png) ### Aurora ![Aurora theme](/screenshots/aurora.png) ### Caelum ![Caelum theme](/screenshots/caelum.png) ### Argentum ![Argentum theme](/screenshots/argentum.png) --- # Settings Reference The Settings page lets you configure Transmute's appearance, conversion behaviour, default output formats, and data management. Changes to appearance and conversion options take effect after clicking **Save Changes**. --- ## Appearance ### Theme Choose the colour theme applied across the entire application. Transmute ships with seven built-in themes: | Theme | Style | |-------|-------| | **Rubedo** *(default)* | Dark — red accent on dark blue | | **Citrinitas** | Dark — gold accent on deep violet | | **Viriditas** | Dark — green accent on black | | **Nigredo** | Dark — purple accent on black | | **Albedo** | Light — silver tones | | **Aurora** | Light — orange accent on warm cream | | **Caelum** | Light — sky blue accent on cool white | See the [Themes](/docs/themes) page for screenshots of each theme. --- ## Conversion ### Auto-download on Completion **Default: off** When enabled, the browser will automatically trigger a file download as soon as a conversion finishes. When disabled, converted files remain available in the History view for manual download at any time. ### Keep Original Files **Default: on** When enabled, the original uploaded file is retained on disk after a conversion completes, and will continue to appear in the Files view. When disabled, the source file is deleted once the conversion job finishes and only the converted output is kept. ### Default Quality Mappings You can save a default `low`, `medium`, or `high` quality level for any output format that exposes quality options. When you later convert to that format, Transmute applies your saved default automatically unless you explicitly choose a different quality for that conversion. See [Quality Reference](/docs/quality-reference) for the exact encoder settings behind each quality tier. ### Cleanup TTL **Default: on** Enables automatic background cleanup of uploads and conversions after they age out. When toggled off, files and conversion records are kept indefinitely (or until removed manually). ### Cleanup Interval **Default: 60 minutes** · Visible only when Cleanup TTL is enabled · Range: 1–10080 minutes (1 minute to 1 week) The number of minutes Transmute waits before automatically deleting an uploaded file or conversion record. For example, a value of `60` means any file or conversion older than one hour will be removed by the background cleanup task. Use the **−** / **+** buttons or type a value directly into the field. > **Tip:** 1440 minutes = 1 day, 10080 minutes = 1 week. --- ## Save Changes The **Save Changes** button persists all Appearance and Conversion settings to the server. Settings are sent as a `PATCH /api/settings` request and take effect immediately after saving. --- ## Data Management These actions are immediate and irreversible. A confirmation dialog is shown before any deletion takes place. ### Clear Conversions Deletes all completed conversion records and their associated output files from the server. Uploaded source files are not affected. Equivalent to `DELETE /api/conversions/all`. ### Clear Uploads Deletes all uploaded source files from the server. Conversion records and output files are not affected. Equivalent to `DELETE /api/files/all`. --- ## Default Formats Default formats let you pin a preferred output format for a given input type. When a file of that type is uploaded on the Converter page, the output format dropdown will be pre-selected to your configured default, saving you from choosing it manually each time. You can still override the selection per file before starting a conversion. ### Adding a default 1. Select an input format from the left dropdown — only formats that don't already have a default configured are shown. 2. Select the desired output format from the right dropdown. 3. Click **Add**. ### Changing a default Use the output format dropdown in the existing mappings table to choose a different output format. The change is saved immediately. ### Removing a default Click the **×** button on the right side of any row to remove that mapping. The change takes effect immediately and the input format becomes available to configure again. --- # Quality Reference Transmute exposes three quality levels: `low`, `medium`, and `high`. Those labels are intentionally simple in the UI and API, but they do **not** mean the same encoder settings for every format. This page is the canonical reference for what each quality level does in the current implementation. --- ## How Quality Works - Quality is always **output-format specific**. - The same `high` value can mean **CRF 18** for one video output, **320 kbps** for one audio output, **300 DPI page rendering** for PDF-to-image output, **JPEG quality 95** for one image encoder, or **no effect at all** for another converter. - Only formats explicitly marked as supporting quality expose the option in the UI and API metadata. - If you omit `quality`, Transmute either uses your saved default quality for that output format or falls back to the converter's built-in medium-style defaults. The shared quality labels are: | Quality | General intent | |--------|----------------| | `low` | Smaller output, faster encoding, more compression | | `medium` | Balanced default | | `high` | Larger output, slower encoding, less compression | > **Important:** Final file size and perceived quality still depend heavily on the source file, encoder defaults, frame complexity, color depth, transparency, resolution, and container/codec limitations. --- ## API and Settings Usage You can pass quality directly when starting a conversion: ```bash curl -X POST http://localhost:3313/api/conversions \ -H "${AUTH}" \ -H "Content-Type: application/json" \ -d '{ "id": "123e4567-e89b-12d3-a456-426614174000", "output_format": "mp4", "quality": "high" }' ``` Supported values are: - `low` - `medium` - `high` You can also save default quality mappings per output format. If a conversion request does not include `quality`, Transmute checks whether you have a stored default for that output format and applies it automatically. --- ## FFmpeg Outputs The FFmpeg converter handles most audio and video outputs. It uses different quality controls depending on the target format family. ### Video Outputs Using CRF + Preset Applies to these output formats: - `mp4` - `avi` - `mov` - `mkv` - `webm` - `ts` - `3gp` - `ogv` - `f4v` - `m4v` Mapping: | Quality | FFmpeg flags | What it means | |--------|---------------|---------------| | `high` | `-crf 18 -preset slow` | Lower compression, better visual retention, slower encode | | `medium` | `-crf 23 -preset medium` | Default balance | | `low` | `-crf 28 -preset fast` | Higher compression, lower fidelity, faster encode | Technical notes: - `CRF` is a constant-quality mode. Lower numbers mean higher quality and usually larger files. - The `preset` mainly changes encoder speed versus compression efficiency. Slower presets usually produce smaller files at the same CRF. - Because CRF is content-adaptive, these settings do **not** correspond to fixed bitrates. - The same CRF can produce very different file sizes depending on motion, noise, grain, animation, and resolution. Practical interpretation: - `high` is suitable when preserving detail matters more than output size. - `medium` is the normal default for general-purpose delivery. - `low` is useful for smaller previews, quick exports, or constrained storage/bandwidth. ### Video Outputs Using Quantizer Scale Applies to these output formats: - `flv` - `mpeg` - `wmv` - `asf` Mapping: | Quality | FFmpeg flags | What it means | |--------|---------------|---------------| | `high` | `-q:v 2` | Higher visual quality | | `medium` | `-q:v 5` | Middle setting | | `low` | `-q:v 9` | More aggressive compression | Technical notes: - `-q:v` is a codec quantizer control. Lower numbers generally mean better quality and larger files. - This is not a universal scale across all codecs. It is only meaningful within the specific encoder chosen for the output container. - File sizes remain variable because these are still quality-oriented settings rather than hard bitrate caps. ### Lossy Audio Outputs Using Bitrate Applies to these output formats: - `mp3` - `aac` - `wma` - `m4a` - `opus` - `mp2` - `ac3` - `oga` Most of these use the same target bitrate mapping: | Quality | Audio bitrate | |--------|---------------| | `high` | `320k` | | `medium` | `192k` | | `low` | `96k` | `ac3` uses a separate mapping: | Quality | AC-3 bitrate | |--------|---------------| | `high` | `448k` | | `medium` | `256k` | | `low` | `128k` | Technical notes: - FFmpeg sets these with `-b:a`. - For stereo content, `320k` is close to transparent for many listeners in MP3/AAC-style workflows, while `96k` is noticeably more compressed. - `Opus` is generally more efficient than MP3 at the same nominal bitrate, but Transmute still uses the same label mapping for consistency. - Actual perceptual quality varies by codec even when the numeric bitrate is the same. - Lossless formats such as `wav`, `flac`, `aiff`, and `mka` do not expose quality options here. ### FFmpeg Side Effects That Matter These are not direct quality tiers, but they affect the output you get: - Video outputs that do not safely support alpha are forced to `-pix_fmt yuv420p`, which strips transparency and improves compatibility. - Video or animated-image outputs may cap frame rate and resolution for practical reasons. For example, video-to-animated-image conversions use `fps=10` and `scale=320:-1:flags=lanczos`. - `3gp` outputs force `libx264` video plus `aac` audio rather than older default codecs. Those behaviors can affect sharpness, color handling, transparency, and output size independently of the selected quality tier. --- ## PyMuPDF PDF Raster Outputs The PyMuPDF converter handles `pdf` to text-like outputs and now also handles `pdf` to raster-image outputs. For PDF raster output, quality does two things: - It changes the **render DPI** used when rasterizing each PDF page. - For some lossy or quality-tunable target formats, it also changes the **Pillow encoder settings** used when writing the final image. Applies to these PDF output formats: - `png` - `jpeg` - `webp` - `tiff` - `bmp` - `gif` - `ppm` - `pgm` - `pbm` - `tga` - `jp2` - `avif` - `jxl` - `ico` - `dib` - `pcx` - `sgi` - `pnm` ### Render DPI Mapping Applies to **all** PDF raster outputs listed above. | Quality | Render DPI | |--------|-------------| | `high` | `300` | | `medium` | `150` | | `low` | `100` | Technical notes: - This is the biggest quality change for PDF-to-image output. - Higher DPI preserves more detail from the source PDF and usually creates larger images and slower processing. - Even for formats that do not have a dedicated encoder-quality knob, the selected quality still matters because the page is rendered at a different resolution first. ### Additional Encoder Mapping For PDF Raster Outputs Some PDF raster outputs also apply format-specific Pillow save settings after the DPI render step. #### JPEG, WebP, AVIF, JPEG XL Applies to these PDF output formats: - `jpeg` - `webp` - `avif` - `jxl` Mapping: | Quality | Save parameter | |--------|------------------| | `high` | `jpeg/webp/jxl: quality=95`, `avif: quality=90` | | `medium` | `jpeg/webp/jxl: quality=80`, `avif: quality=70` | | `low` | `jpeg/webp/jxl: quality=60`, `avif: quality=50` | #### JPEG 2000 (`jp2`) Mapping: | Quality | Save parameter | |--------|------------------| | `high` | `quality_mode='rates', quality_layers=[5]` | | `medium` | `quality_mode='rates', quality_layers=[20]` | | `low` | `quality_mode='rates', quality_layers=[40]` | Technical notes: - For JPEG 2000 in this pipeline, lower numeric rate values correspond to higher quality. - That means the `high` / `medium` / `low` mapping is the reverse of a simple “bigger number = better quality” intuition. ### PDF Raster Formats Without Extra Encoder Quality Knobs These formats still expose `low` / `medium` / `high` for PDF output because DPI changes, even though they do not currently get a separate encoder-quality value: - `png` - `tiff` - `bmp` - `gif` - `ppm`, `pgm`, `pbm`, `pnm` - `tga`, `ico`, `dib`, `pcx`, `sgi` Additional behavior worth knowing: - `png` enables Pillow `optimize=True`. - `tiff` uses `compression='tiff_deflate'`. - Formats without alpha support are flattened onto a white background before saving. ### PDF Text-Like Outputs For PyMuPDF text-style outputs, quality has no effect: - `txt` - `md` - `html` --- ## Pillow Image Outputs The Pillow converter handles image-to-image conversions and some image-to-document outputs. It only exposes quality for selected lossy or quality-tunable formats. ### JPEG, WebP, AVIF, JPEG XL, HEIF, HEIC Applies to these output formats: - `jpeg` - `webp` - `avif` - `jxl` - `heif` - `heic` Mapping: | Quality | Pillow save parameter | |--------|------------------------| | `high` | `quality=95` | | `medium` | `quality=85` | | `low` | `quality=60` | Technical notes: - Transmute passes Pillow's numeric `quality` option directly. - The meaning of `quality=95` or `quality=60` depends on the encoder plugin behind that format. The scale is similar, but not identical, across JPEG, WebP, AVIF, HEIF/HEIC, and JPEG XL. - Higher values generally preserve more detail and create larger files. - `quality=95` is intentionally below the absolute maximum and avoids some oversized-output edge cases common with `100`. Format-specific caveats: - JPEG does not support alpha; transparent inputs are flattened onto a white background before saving. - AVIF, HEIF, HEIC, and JXL are more modern codecs and usually compress better than JPEG at similar perceived quality, but encode/decode behavior depends on the installed plugin implementations. - WebP quality can still produce either much smaller or much larger files than JPEG depending on content and transparency. ### JPEG 2000 (`jp2`) JPEG 2000 uses a different parameter entirely. Mapping: | Quality | Pillow save parameter | |--------|------------------------| | `high` | `quality_layers=[100]` | | `medium` | `quality_layers=[80]` | | `low` | `quality_layers=[30]` | Technical notes: - Pillow uses `quality_layers` instead of `quality` for JP2. - A single-layer configuration means Transmute writes one target layer rather than a multi-layer progressive quality ladder. - Higher values allocate more quality to that layer, generally increasing output size. - JPEG 2000 behavior depends on the available encoder backend and may differ from baseline JPEG expectations. ### Formats Without Exposed Quality These formats are still converted by Pillow but do not currently expose `low` / `medium` / `high`: - `bmp` - `gif` - `ico` - `pdf` - `qoi` - `dds` - `pbm`, `pgm`, `ppm`, `pnm`, `pfm` - `tga`, `sgi`, `icns`, `msp`, `xbm`, `blp`, `dib`, `eps`, `apng`, `mpo`, `pcx` Some of those still have internal normalization rules, optimization flags, palette conversion, or size caps, but they do not use the shared quality selector. --- ## Draw.io Exports The Draw.io converter only supports quality tiers for `jpeg` output. Mapping: | Quality | Draw.io CLI flag | |--------|-------------------| | `high` | `--quality 90` | | `medium` | `--quality 80` | | `low` | `--quality 60` | Technical notes: - These values are passed to the Draw.io CLI during export. - `png`, `pdf`, and `svg` exports ignore the quality setting. - `png` exports always add `--transparent`, which affects the resulting background but is unrelated to quality. - The Draw.io converter always exports page `0`, meaning the first page of the diagram. --- ## LibreOffice Presentation Exports The LibreOffice converter only uses quality tiers when exporting presentations to a single stitched `jpeg` image. ### JPEG Output From Presentations Applies to: - `pptx`, `pptm`, `ppt`, `pps`, `ppsx`, `pot`, `potx`, `odp`, `key` input - `jpeg` output Mapping: | Quality | Pillow JPEG quality | |--------|----------------------| | `high` | `95` | | `medium` | `80` | | `low` | `50` | Technical notes: - LibreOffice first renders the presentation to PDF. - PyMuPDF then rasterizes each page at **200 DPI**. - All rendered pages are stitched into one tall RGB image. - The final combined image is then saved as JPEG with the quality value above. This means the final output quality is influenced by several stages: - LibreOffice's PDF rendering quality - the fixed 200 DPI rasterization step - optional downscaling if the stitched image exceeds JPEG dimension limits - the final JPEG save quality value Dimension limit behavior: - If the combined image would exceed approximately `65500` pixels in width or height, Transmute scales it down before writing JPEG. - That downscaling can reduce sharpness regardless of whether you chose `high`. ### PNG and EPS Output From Presentations - `png` and `eps` outputs use the same PDF-to-image pipeline but ignore the quality selector. - `txt`, `html`, `pdf`, `ppt`, `pptx`, and `odp` outputs also ignore quality. --- ## When Quality Has No Effect Even though the API field is always named `quality`, many converters ignore it entirely. For the converters covered here, quality has **no effect** for: - FFmpeg lossless-style outputs such as `wav`, `flac`, `aiff`, and `mka` - PyMuPDF outputs `txt`, `md`, and `html` - Draw.io outputs `png`, `pdf`, and `svg` - LibreOffice outputs other than stitched `jpeg` - Pillow outputs that are not listed in the quality-enabled set above If a format does not advertise quality support, the UI should not offer the selector and the converter will simply use its normal defaults. --- ## Choosing a Quality Level Use `low` when you want smaller files, faster processing, or preview-grade output. Use `medium` when you want the default balance and do not have strict archival or bandwidth requirements. Use `high` when preserving visible or audible fidelity matters more than encode time and output size. For users who want reproducible automation, point them to this page and have them treat `quality` as a stable Transmute abstraction rather than as a promise of identical encoder math across all formats. --- # API Reference Transmute exposes a REST API so you can automate file conversions without the web UI. All endpoints (except health checks and bootstrap status) require a valid Bearer token — see [Authentication & Users](/docs/authentication) for details on obtaining one. For `curl`, use this header format: ```bash API_KEY="your-api-key-here" AUTH="Authorization: Bearer ${API_KEY}" ``` Then pass `-H "${AUTH}"` on every authenticated request. > **Interactive docs** — Every Transmute instance serves auto-generated ReDoc documentation at [`/api/docs`](http://localhost:3313/api/docs). The full OpenAPI specification is also available on [GitHub](https://github.com/transmute-app/openapi-specifications/blob/main/openapi.json). --- ## Examples & Samples Sample files for testing and real examples using our API can be found in [transmute-app/samples-examples](https://github.com/transmute-app/samples-examples). --- ## Core Workflow The typical automation flow is: **upload → convert → download**. Each step is a single API call. ### 1. Upload a File ```bash curl -X POST http://localhost:3313/api/files \ -H "${AUTH}" \ -F "file=@photo.jpg" ``` **Response** (`200 OK`): ```json { "message": "File uploaded successfully", "metadata": { "id": "123e4567-e89b-12d3-a456-426614174000", "original_filename": "photo.jpg", "media_type": "jpg", "extension": ".jpg", "size_bytes": 204800, "sha256_checksum": "abc123def456...", "compatible_formats": ["png", "gif", "webp", "bmp", "tiff", "ico"] } } ``` The `compatible_formats` array tells you exactly which output formats are supported for this file. Use one of these values in the next step. --- ### 2. Start a Conversion ```bash curl -X POST http://localhost:3313/api/conversions \ -H "${AUTH}" \ -H "Content-Type: application/json" \ -d '{ "id": "123e4567-e89b-12d3-a456-426614174000", "output_format": "png", "quality": "high" }' ``` **Response** (`200 OK`): ```json { "id": "987fcdeb-51a2-43f1-b789-123456789abc", "original_filename": "photo.png", "media_type": "png", "extension": ".png", "size_bytes": 153600, "sha256_checksum": "def789abc123..." } ``` The returned `id` is the converted file's identifier — use it to download the result. The `quality` field is optional and only applies to output formats that support quality tiers. Supported values are `low`, `medium`, and `high`. See [Quality Reference](/docs/quality-reference) for the exact format-specific encoder settings. --- ### 3. Download the Result ```bash curl -OJ http://localhost:3313/api/files/987fcdeb-51a2-43f1-b789-123456789abc \ -H "${AUTH}" ``` The response is the raw file binary (`application/octet-stream`). --- ### 4. Batch Download as ZIP If you have multiple converted files, you can download them all at once as a ZIP archive: ```bash curl -X POST http://localhost:3313/api/files/batch \ -H "${AUTH}" \ -H "Content-Type: application/json" \ -d '{ "file_ids": [ "987fcdeb-51a2-43f1-b789-123456789abc", "aabbccdd-1122-3344-5566-778899aabbcc" ] }' \ --output converted_files.zip ``` The response is an `application/zip` archive containing all requested files. --- ## Other Endpoints ### List Uploaded Files ```bash curl http://localhost:3313/api/files \ -H "${AUTH}" ``` Returns a `files` array with metadata for every uploaded file. ### List Completed Conversions ```bash curl http://localhost:3313/api/conversions/complete \ -H "${AUTH}" ``` Returns a `conversions` array with metadata for every completed conversion, including reference to the original file. ### Delete a File ```bash curl -X DELETE http://localhost:3313/api/files/{file_id} \ -H "${AUTH}" ``` ### Delete a Conversion ```bash curl -X DELETE http://localhost:3313/api/conversions/{conversion_id} \ -H "${AUTH}" ``` ### Delete All Files / Conversions ```bash # Delete all uploaded files curl -X DELETE http://localhost:3313/api/files/all \ -H "${AUTH}" # Delete all conversions curl -X DELETE http://localhost:3313/api/conversions/all \ -H "${AUTH}" ``` --- ## Health Check Endpoints Use these for monitoring and container orchestration (Docker health checks, Kubernetes probes, etc.). ### Liveness — `GET /api/health/live` Confirms the server process is running. ```bash curl http://localhost:3313/api/health/live ``` ```json { "status": "alive" } ``` ### Readiness — `GET /api/health/ready` Confirms the server is ready to handle requests (database and storage are accessible). ```bash curl http://localhost:3313/api/health/ready ``` ```json { "status": "ready", "checks": { "database": "ok", "storage": "ok" } } ``` Returns `503` if any check fails. ### App Info — `GET /api/health/info` Returns the application name and version. ```bash curl http://localhost:3313/api/health/info ``` ```json { "name": "Transmute", "version": "v1.0.0" } ``` --- ## Settings You can read and update application settings (theme, auto-download, cleanup TTL, etc.) via the API. ### Get Current Settings ```bash curl http://localhost:3313/api/settings \ -H "${AUTH}" ``` ```json { "theme": "rubedo", "auto_download": false, "keep_originals": true, "cleanup_ttl_minutes": 60 } ``` ### Update Settings ```bash curl -X PATCH http://localhost:3313/api/settings \ -H "${AUTH}" \ -H "Content-Type: application/json" \ -d '{ "theme": "nigredo", "auto_download": true }' ``` Only the fields you include are updated. ### Default Quality Endpoints Transmute also exposes per-format default quality mappings: | Method | Endpoint | Description | |--------|----------|-------------| | `GET` | `/api/default-qualities` | List your saved default quality mappings | | `POST` | `/api/default-qualities` | Create or update a default quality mapping | | `DELETE` | `/api/default-qualities/{output_format}` | Delete a default quality mapping | Example request: ```bash curl -X POST http://localhost:3313/api/default-qualities \ -H "${AUTH}" \ -H "Content-Type: application/json" \ -d '{ "output_format": "mp4", "quality": "medium" }' ``` See [Quality Reference](/docs/quality-reference) for what each level means per format. --- ## Quick Reference | Method | Endpoint | Description | |--------|----------|-------------| | `POST` | `/api/files` | Upload a file | | `GET` | `/api/files` | List uploaded files | | `GET` | `/api/files/{id}` | Download a file | | `DELETE` | `/api/files/{id}` | Delete a file | | `DELETE` | `/api/files/all` | Delete all files | | `POST` | `/api/files/batch` | Batch download as ZIP | | `POST` | `/api/conversions` | Start a conversion | | `GET` | `/api/conversions/complete` | List completed conversions | | `DELETE` | `/api/conversions/{id}` | Delete a conversion | | `DELETE` | `/api/conversions/all` | Delete all conversions | | `GET` | `/api/health/live` | Liveness check | | `GET` | `/api/health/ready` | Readiness check | | `GET` | `/api/health/info` | App metadata | | `GET` | `/api/settings` | Get settings | | `PATCH` | `/api/settings` | Update settings | | `GET` | `/api/default-qualities` | List default quality mappings | | `POST` | `/api/default-qualities` | Create or update a default quality mapping | | `DELETE` | `/api/default-qualities/{output_format}` | Delete a default quality mapping | --- # From URL Transmute can download a file directly from a URL, add it to your account, and then convert it like any other upload. This is useful when the source file already lives online and you do not want to download it manually first. --- ## What It Does The **From URL** feature accepts a URL, fetches the remote media on the server, and imports the result into Transmute so you can convert it normally. In practice, this covers two common cases: - **Direct file URLs** such as hosted audio, video, image, or document files - **Page URLs** from supported media sites, using the bundled `yt-dlp` downloader Once the download finishes, the file is treated like a normal Transmute upload. --- ## yt-dlp Integration Transmute includes `yt-dlp` in the container image so it can download media from sites that `yt-dlp` supports, including services such as YouTube, Vimeo, and SoundCloud. That support depends on `yt-dlp` itself. If `yt-dlp` can extract a downloadable media stream from the URL you provide, Transmute can import that result and continue with conversion. This also includes playlist URLs, such as YouTube playlists and playlists from other supported sites, anywhere `yt-dlp` can handle them. > **Note:** Transmute is not affiliated with `yt-dlp` in any way. It is simply an additional downloader bundled into the image for convenience. --- ## Using It 1. Open the **From URL** option in the Transmute interface. 2. Paste the source URL. 3. Submit the download. 4. Wait for Transmute to fetch and import the media. 5. Convert the imported file as you normally would. If the URL points to a regular file, Transmute downloads that file directly. If the URL is a supported media page or playlist, Transmute uses `yt-dlp` to resolve and download the media first. --- ## Important Legal Note Use this feature responsibly. Transmute does **not** encourage piracy, copyright infringement, or downloading media you do not have the right to access, copy, or convert. You are responsible for ensuring that your use of the **From URL** feature complies with the law, the source site's terms, and the rights of the content owner. --- ## Limits and Expectations - Support for site downloads and playlists is only as broad as current `yt-dlp` support. - Some URLs may fail because the source site changed, blocks automated access, requires authentication, or is otherwise unsupported. - A successful download still needs to be in a format that Transmute can process and convert. If a URL fails, try a direct file URL when available, or verify whether the source is currently supported by `yt-dlp`. --- ## Authenticated Downloads Some sources require authentication to fetch a file (private file servers, internal artifact stores, protected APIs, etc.). Transmute supports per-domain authentication for direct HTTP/HTTPS downloads via a JSON config file at `domain_auth/config.json` inside the app's working directory. When the HTTP downloader is about to fetch a URL, it looks up the URL's host (and port, if present) in this config. If a matching entry is found, the configured credentials are attached to the request automatically. > **Note:** Domain auth currently applies to direct HTTP/HTTPS downloads only. It is not used by the `yt-dlp` downloader. ### Config Format `domain_auth/config.json` is a JSON array of objects. Each object describes one domain and the credentials to use for it: ```json [ { "domain": "files.example.com", "auth_type": "basic", "secret": "alice:s3cret" }, { "domain": "api.example.com", "auth_type": "bearer", "secret": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.payload.signature" }, { "domain": "downloads.example.com:8443", "auth_type": "bearer token", "secret": "ghp_exampleTokenValue1234567890" }, { "domain": "internal.example.com", "auth_type": "header", "secret": "X-Api-Key: abc123-def456-ghi789" }, { "domain": "cdn.example.com", "auth_type": "custom header", "secret": "X-Auth-Token: my-secret-token-value" } ] ``` Each entry has three fields: - **`domain`** — The host to match. May include a port (`example.com:8443`). Matching is case-insensitive. An exact `host:port` entry is preferred over a host-only entry when both are present. - **`auth_type`** — How to authenticate. See [Supported Auth Types](#supported-auth-types). - **`secret`** — The credential material. The exact format depends on `auth_type`. Invalid or unrecognized entries are skipped with a warning and do not prevent other entries from being used. ### Supported Auth Types | `auth_type` value | Behavior | `secret` format | | --- | --- | --- | | `basic`, `username and password`, `username_password` | HTTP Basic auth | `"username:password"` | | `bearer`, `bearer auth`, `bearer token`, `token` | Sends `Authorization: Bearer ` | The raw token | | `header`, `custom header` | Sends a literal header on the request | `"Header-Name: value"` | `auth_type` matching is case-insensitive. ### Docker Compose Example The `domain_auth` config lives outside the data volume. Mount the **folder** (not the file itself) so you can create or edit the config from the host without recreating the container: ```yaml services: transmute: image: ghcr.io/transmute-app/transmute:latest container_name: transmute restart: unless-stopped ports: - 3313:3313 volumes: - transmute_data:/app/data - ./domain_auth:/app/domain_auth healthcheck: test: - "CMD" - "wget" - "-q" - "-O" - "/dev/null" - "--tries=1" - "http://localhost:3313/api/health/ready" interval: 30s timeout: 10s retries: 3 start_period: 40s volumes: transmute_data: ``` Then, on the host, create the folder and the config file **after** the bind mount is in place: ```sh mkdir -p ./domain_auth cat > ./domain_auth/config.json <<'EOF' [ { "domain": "files.example.com", "auth_type": "basic", "secret": "alice:s3cret" } ] EOF docker compose up -d ``` > **Why mount the folder, not the file?** If you bind-mount `./domain_auth/config.json` directly and the file does not yet exist on the host, Docker will create a *directory* with that name on the host, which breaks the config. Mounting the folder lets you add, edit, or remove the JSON file at any time and have Transmute pick it up on the next download. > **Security:** The config file contains plaintext credentials. Restrict its permissions on the host (e.g. `chmod 600 ./domain_auth/config.json`) and do not commit it to source control. --- # Environment Variables Transmute is configured through environment variables. Set them in the `environment` section of your `docker-compose.yml`, in a `.env` file mounted into the container, or as standard OS environment variables. Variable names are **case-insensitive**. --- ## Example Docker Compose ```yaml services: transmute: image: ghcr.io/transmute-app/transmute:main container_name: transmute restart: unless-stopped ports: - 3313:3313 volumes: - transmute_data:/app/data environment: - AUTH_SECRET_KEY=replace-with-a-long-random-string - AUTH_ACCESS_TOKEN_EXPIRE_MINUTES=120 - APP_URL=https://transmute.domain.com # - APP_URL=http://http://192.168.1.113:3313 - PORT=3313 volumes: transmute_data: ``` --- ## Reference ### Authentication | Variable | Default | Description | |----------|---------|-------------| | `AUTH_SECRET_KEY` | *(auto-generated)* | Secret key used to sign JWT tokens. If not set, a random 64-byte key is generated on every startup — meaning all existing tokens are invalidated when the container restarts. **Set this to a fixed value** for persistent sessions across restarts. | | `AUTH_ALGORITHM` | `HS256` | Algorithm used for JWT signing. Unless you have a specific reason to change this, leave it as the default. | | `AUTH_ACCESS_TOKEN_EXPIRE_MINUTES` | `60` | How long a JWT access token remains valid, in minutes. | | `ALLOW_UNAUTHENTICATED` | `false` | Allow users to use the app as a temporary guest account. WARNING: DO NOT EXPOSE TRANSMUTE PUBLICALLY IF YOU ENABLE THIS. | > **Important:** If you don't set `AUTH_SECRET_KEY`, a new random key is generated each time the container starts. This means all logged-in users will be signed out and all existing JWTs will stop working after a restart. For production use, always set a fixed `AUTH_SECRET_KEY`. **Generating a strong secret key:** ```bash # Using Python python3 -c "import secrets; print(secrets.token_urlsafe(64))" # Using OpenSSL openssl rand -base64 64 ``` ### OIDC For OIDC configuration variables, please see [OIDC / SSO Integration](/docs/oidc/). ### Server | Variable | Default | Description | |----------|---------|-------------| | `HOST` | `0.0.0.0` | Network interface the server binds to. The default binds to all IPv4 interfaces, which is required inside Docker. | | `HOSTS` | `""` | A list of network interfaces the server binds to, either comma-separated or in JSON array format. If it is not set, the `HOST` value is used instead. | | `PORT` | `3313` | Port the server listens on inside the container. If you change this, update the `ports` mapping and health check URL in your Compose file to match. | | `APP_URL` | `""` | Public URL of the app. Used for constructing URLs in the OIDC API response and for the displayed URL in the API docs. If it includes a path (e.g. `https://host/transmute`), Transmute is served under that [sub-path](/docs/reverse-proxy). | | `CONVERSION_WORKER_CONCURRENCY` | `5` | Number of concurrent conversion workers. Each one runs on its own thread. | > **What is the distinction between `HOSTS` and `HOST`?** > > Simple configuration that require the server to bind to only one interface can be done using `HOST`. However, more complex configurations may require binding to more than one interface, which `HOSTS` enables; one example of this is if you wish to make the server available over both IPv6 and IPv4, which requires binding to an address of each family. For example, binding to all IPv6 interfaces and all IPv4 interfaces requires binding to both `::` and `0.0.0.0`, which can be done by setting `HOSTS` to `::,0.0.0.0` or `["::", "0.0.0.0"]`. > > If both `HOST` and `HOSTS` are used at the same time, the value set in `HOSTS` will be used, and a warning to this effect will be logged. ### Storage | Variable | Default | Description | |----------|---------|-------------| | `DATA_DIR` | `data` | Base directory for all persistent data (database, uploads, outputs, temp files). Inside Docker this is typically `/app/data` and should be backed by a volume. | > **Tip:** You generally don't need to change `DATA_DIR` when using Docker — just mount a volume to `/app/data` as shown in the example Compose file above. > **Related:** Custom PDF styling uses a file under the data directory (`pdf/custom.css`), not an environment variable. See [PDF Styling](/docs/pdf-styling/). ### Database Table Names These control the SQLite table names. You should not change them unless you have a very specific reason. | Variable | Default | Description | |----------|---------|-------------| | `FILE_TABLE_NAME` | `FILES_METADATA` | Table for uploaded file metadata | | `CONVERSION_TABLE_NAME` | `CONVERSIONS_METADATA` | Table for conversion output metadata | | `CONVERSION_RELATIONS_TABLE_NAME` | `CONVERSION_RELATIONS` | Table linking conversions to source files | | `APP_SETTINGS_TABLE_NAME` | `APP_SETTINGS` | Table for per-user application settings | | `USER_TABLE_NAME` | `USERS` | Table for user accounts | --- # UI Translations Transmute supports community-contributed UI translations. Currently supported languages are: - English - Spanish (Shoutout to [@Fuan200](https://github.com/Fuan200) for initial translation) - German (Shoutout to [@AnguTom](https://github.com/AnguTom) for initial translation) - Polish (Shoutout to [@Icikowski](https://github.com/Icikowski) for initial translation) - Italian (Shoutout to [@StefanoPernat](https://github.com/StefanoPernat) for initial translation) - French (Shoutout to [@bastiengrignon](https://github.com/bastiengrignon) for initial translation) - Danish (Shoutout to [@eurosite](https://github.com/eurosite) for initial translation) - Hindi (Shoutout to [@ykd007](https://github.com/ykd007) for initial translation) - Czech (Shoutout to [@halisovaprace](https://github.com/halisovaprace) for initial translation) - Turkish (Shoutout to [@Elvin0802](https://github.com/Elvin0802) for initial translation) - Azerbaijani (Shoutout to [@Elvin0802](https://github.com/Elvin0802) for initial translation) - Portuguese (Shoutout to [@Ian-Marcel](https://github.com/Ian-Marcel) for initial translation) If you want to add a new language, open a pull request similar to [this example](https://github.com/transmute-app/transmute/pull/109/changes). ## How to Add a Translation 1. Add a new JSON file in [`frontend/src/i18n`](https://github.com/transmute-app/transmute/tree/main/frontend/src/i18n). 2. Use [`frontend/src/i18n/en.json`](https://github.com/transmute-app/transmute/blob/main/frontend/src/i18n/en.json) as the source file and match its keys exactly. 3. Register the new language in [`frontend/src/i18n/index.ts`](https://github.com/transmute-app/transmute/blob/main/frontend/src/i18n/index.ts). 4. Open a pull request with the new translation file and the index update. ## Important Notes - The translation file should keep the same structure and keys as `en.json`. - If a key is missing or renamed, parts of the UI may fall back to English or fail to display the expected text. - It helps to mention the language code and native language name in the pull request description. ## Maintainer Note English is the only language I can personally review. Any non-English translations included in Transmute were supplied by users and other contributors. --- # Reverse Proxy & Sub-path Transmute can be served either at a **domain root** (`https://transmute.example.com`) or under a **sub-path** (`https://example.com/transmute/`). Both are configured with the single [`APP_URL`](/docs/environment-variables) variable, the sub-path is derived from its path component automatically: | `APP_URL` | Served at | | ---------------------------- | ---------------- | | *(unset)* or `https://host` | `/` (the default)| | `https://host/transmute` | `/transmute/` | ```yaml # docker-compose.yml services: transmute: image: ghcr.io/transmute-app/transmute:main environment: - APP_URL=https://example.com/transmute ``` ## How it works Transmute serves the **whole app** (frontend, API and docs) under the sub-path itself. The reverse proxy therefore only needs to forward the request path **as-is**. > **Important:** Do **not** strip the sub-path prefix at the proxy. Transmute > expects to receive the full `/transmute/...` path. Stripping it (e.g. an nginx > `proxy_pass` with a trailing slash, or a Traefik `StripPrefix` middleware) will > break asset loading. No rebuild is required: asset URLs, the frontend router, API calls and the OpenAPI/docs URLs all adapt to `APP_URL` at runtime. ## nginx Note the **absence of a trailing slash** on `proxy_pass`, which preserves the full path: ```nginx location /transmute/ { proxy_pass http://127.0.0.1:3313; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; } ``` ## Traefik Route the path prefix to the service **without** a `StripPrefix` middleware: ```yaml labels: - "traefik.enable=true" - "traefik.http.routers.transmute.rule=Host(`example.com`) && PathPrefix(`/transmute`)" - "traefik.http.services.transmute.loadbalancer.server.port=3313" ``` ## OIDC under a sub-path OIDC works under a sub-path with no extra configuration, the callback URL is derived from `APP_URL`. Make sure the redirect URI registered with your provider includes the sub-path, e.g. `https://example.com/transmute/api/oidc/callback`. See [OIDC / SSO Integration](/docs/oidc) for the full setup. > **Tip:** Leaving `APP_URL` unset, or pointing it at a bare host, serves the app > at the root exactly as before, no migration needed. --- # PDF Styling When Transmute converts Markdown and other documents to PDF (via Pandoc / WeasyPrint), it applies a built-in compact stylesheet by default. That reduces large fonts and margins so short documents do not spill across unnecessary pages. You can override that styling with your own CSS. No environment variable or Settings UI option is required — place a file at a conventional path under the data directory, or bind-mount one in Docker. --- ## Default Behaviour If `data/pdf/custom.css` is **not** present, Transmute uses its built-in compact CSS for PDF output. If that file **is** present, Transmute uses it instead for document → PDF conversions. --- ## Custom CSS Path | Location | Path | |----------|------| | On disk (relative to `DATA_DIR`) | `pdf/custom.css` | | Inside the Docker container | `/app/data/pdf/custom.css` | The `pdf/` folder under the data directory is created automatically on startup. You only need to add `custom.css`. Your stylesheet can set page size, margins, fonts (including `@font-face`), heading sizes, list spacing, and other print-oriented rules. --- ## Docker: Bind-Mount a File Mount a host stylesheet directly onto the conventional path: ```yaml services: transmute: image: ghcr.io/transmute-app/transmute:latest container_name: transmute restart: unless-stopped ports: - 3313:3313 volumes: - transmute_data:/app/data - ./pdf-custom.css:/app/data/pdf/custom.css:ro healthcheck: test: - "CMD" - "wget" - "-q" - "-O" - "/dev/null" - "--tries=1" - "http://localhost:3313/api/health/ready" interval: 30s timeout: 10s retries: 3 start_period: 40s volumes: transmute_data: ``` Create `./pdf-custom.css` on the host **before** starting the stack. If you bind-mount a path that does not exist yet, Docker may create a directory with that name instead of a file. --- ## Docker: Use the Data Volume If you already mount the data volume to the host (or can copy into it), place the file at `pdf/custom.css` inside that volume — for example `/app/data/pdf/custom.css` in the container. No extra bind mount is needed. --- ## Notes - This applies to document → PDF conversions that go through the Pandoc / WeasyPrint path (for example Markdown → PDF). - Removing or renaming `custom.css` falls back to the built-in compact stylesheet on the next conversion. - Related storage layout is controlled by `DATA_DIR`; see [Environment Variables](/docs/environment-variables/).