A small, self-hosted file transfer portal built on VOT (Verified Object Transfer).
You sign in to the admin UI, open Receive, create a unique request link (with an optional password), and send it to someone. They open it in a browser, drop files on the page, and the files land, cryptographically verified, atomically published, never overwriting anything, in a folder you chose on your server.
you (admin) them (any modern browser)
│ │
│ create request link │
│ https://drop.example/r/9f2c… │
├──────────── send link ───────────▶│
│ │ vot-wasm hashes the files locally,
│ │ builds a VOT package (manifest+seal),
│ │ streams up to eight proven 8 MiB ranges
│ ▼
│ votport server (this repo)
│ verifies every range against the announced
│ package root before a byte is accepted;
│ publishes atomically via vot-sdk-file
▼
/your/folder/… ← files appear here, listed in the admin UI
Received files use Send beside a file on Receive. For the outbound library, use Deliver to choose one or more files. Server-rendered projects may populate nested project subdirectories under the outbound directory, and admin uploads land there too. VOTPort issues one expiring bearer URL for the selection, available to copy again from Deliver and revocable by an admin. Before an individual file download starts, the server verifies the source's VOT object identity and signed receipt against a cached proof catalog (built once per object), then streams verified ranges directly from the source. ZIP and grouped batch downloads copy each source into private staging and verify it per request. Individual files support RFC 9110 single-byte-range requests for resumable downloads; a file counts once, when a response reaching its last byte completes, and a short-lived secure lease permits the ranges before it. For multi-file deliveries, recipients may download a payload-only ZIP or stream the files separately in bulk; signed receipts remain optional individual downloads. Optional link passwords gate metadata, files, receipts, and the ZIP.
Folder downloads keep existing files and directories by using numbered names.
- End-to-end integrity. The browser computes each file's VOT object identity (BLAKE3 merkle root) and a package manifest before anything is sent. The server accepts a byte range only with a valid proof against the announced root — a flipped bit in transit, a truncated body, or a buggy proxy is refused, not stored.
- Authenticated names. File names arrive inside the sealed manifest, so the listing you see is the listing that was hashed.
- Atomic, no-overwrite publication. Files are staged and published by
vot-sdk-file: a file either appears complete and verified, or not at all. Existing files are never overwritten — repeats getname-1.ext. - Interrupted uploads leave nothing behind. Abandoned sessions are swept and their staging files removed.
Requirements: Docker with the compose plugin, and Caddy on the host.
git clone https://github.com/halideworks/votport
cd votport
install -m 600 .env.example .env
# edit .env: set VOTPORT_ADMIN_PASSWORD, VOTPORT_METRICS_TOKEN and the https URL Caddy will serve
sudo install -d -o 1000 -g 1000 -m 0700 data received outbound
docker compose --env-file .env -f docker-compose.example.yml up -d --buildThe three bind roots must be writable by uid/gid 1000, the user inside the
container. If they already exist, grant that uid/gid access without
recursively changing unrelated content. The tracked docker-compose.yml is a
deployment-specific template; the example file is the portable quick start.
The first build takes a while: it compiles the VOT SDK to WebAssembly for the
browser and builds the server. Then add the site to your Caddyfile (see
Caddyfile.example):
drop.example.com {
respond /metrics 404
respond /readyz 404
reverse_proxy 127.0.0.1:8103
}Reload Caddy, open https://drop.example.com, sign in with your admin
password, create a link, and send it to someone.
Default-tenant files appear under the host folder mounted at /received.
Named tenants use /received/.vot-tenants.stage/<tenant>/; per-link
subfolders are configurable when you create a link.
The release workflow publishes semver tags and commit tags to GHCR, never
latest. Pin a customer deployment to the digest recorded in the GitHub
release notes and workflow summary:
services:
votport:
image: ghcr.io/halideworks/votport:vX.Y.Z@sha256:<published-digest>
# remove build: .Keep the complete image reference, including its digest, with the matching
database and /received and /outbound backup set. System-page automatic
archives cover the database and VOTPort-managed identity files only; the two
content volumes still need operator-owned file backups.
Environment variables are the boot defaults. The portable Docker quick start
uses .env with docker-compose.example.yml; deployment-specific Compose
files may use different values. A platform admin can configure the SMTP relay,
retention, and default quotas
from System without SSH (GET/PUT /api/admin/settings). Configure named
notification destinations and tenant defaults on Notifications.
The System page configures automatic local and S3-compatible backups, reports
redacted status, and stages restores for the service supervisor. Backup archives include
the database and VOTPort-managed identity files only; a blank local path uses
<data_dir>/backups (normally /data/backups), while a custom path must be a
writable service-filesystem path mounted by the operator. S3 prefix settings
cover VOTPort backup objects only, and /received and /outbound remain
operator backups. Archives are copies, not a tomb: erased content stays
restorable from every archive that still contains it until each copy is
deleted (DELETE /api/admin/backups/{source}/{id}) or ages out; retention 0
keeps archives forever.
Optional backup secrets are unchanged when left blank and never returned by
GET. A restore rotates the cookie secret and signs out existing sessions. A
written settings key wins; "" disables a URL or token; JSON null deletes
the row so env applies again. Details: docs/deployment.md.
Boolean settings accept 1/true/yes/on and 0/false/no/off, ignoring case and
surrounding whitespace. Unset settings use the defaults below; empty or invalid
boolean values stop startup.
The table covers common settings. See the configuration references for session limits, SSO roles and session lifetime, JSON logs, and workflow snapshot budgets.
VOTPORT_SESSION_IDLE_SECS defaults to 1800 seconds. Choose a value longer
than the longest sender or network pause when preserving interrupted transfers;
the portable Compose example uses 172800 seconds (48 hours). Keep this
environment setting the same on a promoted failover host. Active requests
remain retained while in flight.
| Variable | Default | Meaning |
|---|---|---|
VOTPORT_ADMIN_PASSWORD |
— | Admin password (hashed with argon2id at startup). Required unless the hash is set. At least 12 characters; a shorter one refuses to start, because this is the credential that still works when the identity provider does not. |
VOTPORT_ADMIN_PASSWORD_HASH |
— | Complete supported Argon2 PHC string; malformed hashes stop startup. Takes precedence over the plain password. |
VOTPORT_PUBLIC_URL |
— | Public https origin with no path prefix; used for generated links and to mark cookies Secure. |
VOTPORT_BIND |
0.0.0.0:8080 |
Listen address inside the container. |
VOTPORT_PUSH_BIND |
off | UDP address for native VOT pushes. Setting it enables the listener; leave unset to keep native push disabled. |
VOTPORT_PUSH_ADVERTISE |
derived | host:port that native senders dial. Defaults to the host in VOTPORT_PUBLIC_URL and the push bind port. Set it when the public UDP address differs. |
VOTPORT_SERVE_BIND |
unset | UDP address for serving Deliver grants to VOT clients over QUIC; off when unset. Same certificate and issuer key as push. |
VOTPORT_SERVE_ADVERTISE |
derived | host:port that VOT fetch clients dial, on the same rules as VOTPORT_PUSH_ADVERTISE. |
VOTPORT_PUSH_CERT / VOTPORT_PUSH_KEY |
generated | PEM certificate and private key for native push. Set both together to use those paths in place, or leave both unset for a persistent self-signed pair under VOTPORT_DATA_DIR. |
VOTPORT_DATA_DIR |
/data |
State: votport.db (links and upload records; current schema required) and the cookie secret. |
VOTPORT_RECEIVE_DIR |
/received |
Root folder received files are published into. |
VOTPORT_OUTBOUND_DIR |
/outbound |
Root folder for server-rendered projects and files uploaded by admins for outbound links. Nested project subdirectories are allowed. |
VOTPORT_LIBRARY_PREHASH |
true |
Prepare roots and proof catalogs for stable Library files while transfers are idle. Polls every 15 seconds between hashes, skips temporary files and symlinks, and warms the newest 4,096 discovered eligible sources. Set to false to disable background disk reads. |
VOTPORT_MAX_UPLOAD_BYTES |
50 GiB | Hard cap per upload session (per-link caps can be lower). Accepts plain bytes or a K/KiB/KB, M/MiB/MB, G/GiB/GB, T/TiB/TB suffix, e.g. 500G. |
VOTPORT_ALLOW_HIDDEN |
off | Set 1 to accept dot-file names from uploaders. |
VOTPORT_SESSION_IDLE_SECS |
1800 |
Positive idle time in seconds before an unfinished upload session is discarded; zero stops startup. |
VOTPORT_WEB_ROOT |
./web |
Static assets directory (/app/web in Docker). |
VOTPORT_NOTIFY_SMTP_HOST |
— | Shared SMTP relay host. Configure recipients on named email destinations. |
VOTPORT_NOTIFY_SMTP_PORT |
587 |
SMTP port. Port 465 uses implicit TLS. |
VOTPORT_NOTIFY_SMTP_STARTTLS |
on | SMTP STARTTLS. Set 0 or false to disable. Port 465 uses implicit TLS regardless. |
VOTPORT_NOTIFY_SMTP_USERNAME |
— | Optional SMTP AUTH username. |
VOTPORT_NOTIFY_SMTP_PASSWORD |
— | Optional SMTP AUTH password. |
VOTPORT_SCIM_TOKEN |
— | Bearer for the SCIM 2.0 provisioning endpoint at /scim/v2; unset disables it. Overridable via PUT /api/admin/settings. |
VOTPORT_REPLICA_TOKEN |
— | Bearer a standby presents to GET /api/replica; unset disables it. Overridable via PUT /api/admin/settings. |
VOTPORT_STANDBY_SOURCE |
— | votport standby only: the live instance's https URL to pull replicas from. |
VOTPORT_STANDBY_INTERVAL_SECS |
60 |
votport standby only: seconds between pulls; the RPO. |
VOTPORT_SCIM_REQUIRE_PROVISIONING |
0 |
1 refuses SSO sign-in for subjects with no principal row. Overridable via PUT /api/admin/settings. |
VOTPORT_OIDC_SUBJECT_CLAIM |
sub |
Id-token claim used as the principal subject: sub, email, or preferred_username. Must match the SCIM userName mapping. |
VOTPORT_NOTIFY_SMTP_FROM |
— | SMTP From address (required with the relay host). |
VOTPORT_AUDIT_RETENTION_DAYS |
400 |
Days to keep queryable audit rows; 0 disables pruning. Overridable via PUT /api/admin/settings. |
VOTPORT_UPLOAD_RETENTION_DAYS |
off | Days to keep received files; a daily sweep deletes expired content, blanks the records' in-package path, and audits it. The record itself (size and content identity) stays until the link is cleared. 0 (default) keeps everything. Overridable via PUT /api/admin/settings. |
VOTPORT_DEFAULT_MAX_TOTAL_BYTES |
unlimited | Fills a new tenant's byte quota when the request omits it, and caps received bytes on the unnamed default tenant. Named tenants keep the quota on their row. |
VOTPORT_DEFAULT_MAX_LINKS |
unlimited | Same overlay for max links (new tenants when omitted, and the unnamed default tenant). |
VOTPORT_DEFAULT_MAX_SESSIONS |
unlimited | Same overlay for max concurrent sessions. |
VOTPORT_PUBLIC_PASSWORD_LOGIN |
on | Set 0 to prefer collapsing the local password form when SSO is offered (login API stays available). |
VOTPORT_SSO_SESSION_SECS |
604800 |
SSO session lifetime in seconds, from 1 through 31536000 (365 days). Overridable via PUT /api/admin/settings. |
VOTPORT_METRICS_TOKEN |
— | When set, GET /metrics requires this bearer token. Counts only; scrape over an internal interface. |
VOTPORT_TRUSTED_PROXIES |
loopback + private ranges | Comma-separated CIDR blocks (or bare addresses) whose X-Forwarded-For is believed. Anything else is keyed on its socket address. The default trusts any loopback or RFC1918/ULA peer, which is broad: on a shared container network or a LAN bind, anything that can reach the port can pick its own throttle bucket. Name your reverse proxy to close that; see Client addresses. |
VOTPORT_OIDC_ISSUER |
— | OIDC issuer URL for admin single sign-on. Requires the client id/secret and VOTPORT_PUBLIC_URL; see Single sign-on. |
VOTPORT_OIDC_CLIENT_ID |
— | OAuth client id at the identity provider. |
VOTPORT_OIDC_CLIENT_SECRET |
— | OAuth client secret at the identity provider. |
VOTPORT_OIDC_ADMIN_GROUP |
— | Group whose members sign in as admins. Unset means every principal your provider authenticates gets admin. |
Notifications are best-effort: a delivery failure is logged and never affects the upload.
Production setup beyond the quick start - SSO walkthroughs (Authentik,
Entra ID), backups and restore, metrics scraping, tenant provisioning, and
content lifecycle - lives in docs/deployment.md.
Set VOTPORT_OIDC_ISSUER, VOTPORT_OIDC_CLIENT_ID and
VOTPORT_OIDC_CLIENT_SECRET (plus VOTPORT_PUBLIC_URL, which builds the
redirect URI <public-url>/api/admin/callback) and the login page gains a
Sign in with SSO button alongside the local password form. The flow is
authorization-code with PKCE; the id token is verified against the provider's
JWKS, with issuer, audience and nonce checks.
Roles use provider and SCIM group membership. VOTPORT_OIDC_ADMIN_GROUP
members are administrators; otherwise VOTPORT_OIDC_AUDITOR_GROUP members
get audit-only access, and everyone else is a read-only viewer. Admin
membership takes precedence. When the admin group is unset, every authenticated
principal is an administrator and votport warns at startup.
Local password sign-in always remains available as the break-glass path,
including when SSO is configured. POST /api/admin/login is never disabled.
System can collapse the password form behind a disclosure when SSO is offered;
the form stays in the page. SSO principals appear on /tenants and can be
revoked (current sessions die; further SSO is refused until unblock). Lasting
revoke is removing the IdP group.
Every administrative and transfer event — sign-ins (with client IP),
link lifecycle, received-file deletions, upload completions and failures — is
written both to the structured log (RUST_LOG defaults to
info,votport=info,audit=info; tune per target, e.g. RUST_LOG=audit=debug) and
to an append-only table in the database. Set AUDIT_LOG=/path/audit.log to
route audit-target log events to a dedicated append-only file (same line
format, created on first write) instead of stdout, so the trail survives
dropping other log noise. Export it as JSONL for a SIEM:
curl -b cookies.txt 'https://drop.example.com/api/admin/audit?since=0&limit=1000'Rows are never modified or deleted through the API; retention prunes them
after VOTPORT_AUDIT_RETENTION_DAYS (default 400).
Platform admins can read current link and live-byte totals for every tenant at
GET /api/admin/holdings. Quota checks, holdings, and /metrics use the
schema-v7 SQL file projection rather than loading upload-history JSON.
Every published file gets a sidecar, <name>.vot-receipt: a canonical
vot-receipt CBOR envelope, ed25519-signed
with a key votport generates in the data directory (receipt.key), attesting
that exactly that object (suite, BLAKE3 root, length) reached Published
assurance under the receive filesystem's commit profile (Balanced on local storage
and explicitly qualified Linux SMB/NFS), with the session, provider incarnation,
sequence, and UTC timestamp of the observation. The verifying
public key is shown on System (and returned by GET /api/admin/links as
receipt_key); the receipt's embedded key id is the same 32-byte public key.
Anyone can check a sidecar without an account: open /verify, drop the file
and its sidecar (the file is hashed in your browser and never uploaded), or
post the sidecar bytes to POST /api/verify after fetching the key from
GET /api/receipt-key. Programmatically, verify with the vot-receipt
crate: decode_authenticated(bytes) then verify_ed25519(&decoded, &key).
- Request links are unguessable 128-bit URLs; add a per-link password for a second factor (argon2id-hashed, verified before any upload state exists).
- The admin session is a signed, expiring cookie (
HttpOnly,SameSite=Lax,Securebehind https); mutating admin calls also require a custom header, which closes cross-site request forgery. - Password guessing is throttled per client bucket, and each password path holds its own small budget of concurrent argon2 verifications. Nothing refuses a correct password, so no one can lock the administrator out of the break-glass credential. A sustained flood can still make sign-in wait behind the queue for that budget; the wait is bounded by the queue and ends with the flood, and rate limiting at the reverse proxy is the answer to it. Guessing buckets group an IPv6 client by /64, since a client holding a routed prefix would otherwise get a fresh budget per address; the cost is that neighbours in one prefix share a lockout. Upload-session creation and receipt checks are quotas rather than guessing throttles, so they key on the full address and colleagues in one office do not share a budget.
- Optional SSO sign-in maps your identity provider's groups to admin or
read-only viewer access; session cookies bind the role and are invalidated
by password changes or role changes. Platform admins can revoke an SSO
principal from
/tenants. - Uploaded names pass VOT's portable-path profile (no traversal, no control characters, no Windows-reserved names) and a server-side re-check; files cannot land outside the receive folder.
- Per-link expiry, per-link size caps, a global session cap, and bounded request bodies keep resource use predictable.
- votport itself speaks plain HTTP and expects to sit behind Caddy (or any TLS-terminating proxy) — don't expose the container port publicly.
Threat-model honesty: TLS (Caddy) protects confidentiality in transit. VOT adds integrity — what lands on disk is exactly what the sender's browser hashed, verified range by range, independent of every proxy in between.
- Open the site, sign in (password and, if configured, SSO).
- Receive: issue a request (label, optional destination, password, expiry, size cap). Enable its optional notification policy to be notified when a receive completes or fails. Copy the URL or show a QR code.
- When files arrive, Receive lists uploads as object cards: stored path,
size, and a click-to-copy identity line (
suite:64-hex root) per file, plus whether the file is still on disk and whether its receipt sidecar was written. Delete a file (and its receipt) or clear a transfer from history. Deactivate or delete links when done (files stay until you delete them or retention sweeps them). - Deliver: browse nested project directories and admin-uploaded files,
select one or more files and issue one expiring, revocable download link,
or share a project folder directly and issue one expiring, revocable
download link. Manual selection and direct folder shares can include up to 1,000,000 files.
Library upload requests retry within the current browser or desktop
operation. Selecting a file again starts a fresh upload, so a changed
file cannot reuse another attempt's partial bytes even when its name,
size, and timestamp match. A minute sweep removes partial library uploads
idle longer than
VOTPORT_SESSION_IDLE_SECSwhile preserving active requests. Delivery links have an optional notification policy for the first download and completed delivery. Issued links show aggregate and per-file download starts with first and most recent timestamps. For multi-file links,max_downloadsapplies per file and per full-delivery round. - Tenants (platform admin): namespaces, quotas, principals, revoke.
- Audit: queryable event log and JSONL export.
- System: password, automatic backup configuration/status/restore, database snapshot download, receipt public key, SMTP relay, retention, default quotas.
The navigation also includes Workflows, Trade routes, Storage, Automation, and Notifications for project deliveries, connected ports, storage connections, scoped tokens, and notification destinations. Available pages depend on the signed-in role; auditors see Audit only.
Senders can drop folders as well as files; browser support requires WebAssembly SIMD and module workers (Safari 16.4, Chrome 91, Firefox 114 or newer).
Agents can browse a scoped library folder, create and recover expiring delivery
links, inspect object identities and download activity, and revoke their own
links. Issue a token with explicit permissions from Automation or the desktop
app's Settings > Agent access. The client CLI provides JSON agent commands
and votport mcp; desktop builds bundle the CLI and can copy its MCP
configuration. See agent setup, API, and retry semantics.
Create a tenant-scoped automation token on Automation. The raw token is shown once, so copy it immediately; revoke it and create another if it is lost. Use the token to share a server-relative outbound directory from the CLI (up to 1,000,000 files per share):
export VOTPORT_URL=https://drop.example.com
export VOTPORT_AUTOMATION_TOKEN='<token from Automation>'
# Optional password for the outbound link:
export VOTPORT_SHARE_PASSWORD='use-a-separate-secret'
votport share project/render --expires 7d --label "Client delivery" --max-downloads 1The command prints the expiring share URL. VOTPORT_URL must be the HTTPS
server URL (HTTP is allowed for loopback); VOTPORT_SHARE_PASSWORD is optional.
--max-downloads is optional. For a multi-file delivery it applies per file
and per full-delivery round; 1 allows one download of each file or one full
delivery.
The wire unit is an 8 MiB proven range (CHUNK_BYTES in session.rs). That
ceiling is protocol-level in VOT, not a votport knob. The browser keeps up to
eight range PUTs in flight and hashes ahead in module workers. The server
worker drains up to eight queued ranges into a batch, verifies and writes
them in parallel against the announced Merkle root, then publishes completed
files in a separate pass.
The current VOT pin retains the 8 MiB range ceiling. Do not raise
CHUNK_BYTES without upstream support for larger ranges.
Measure on this box:
cargo test --test e2e -- --ignored --nocapture throughput_baselineLatest run (2026-08-23, this host, 256 MiB single file): hashing and packaging 3012 MiB/s, loopback upload 1203 MiB/s.
SQLite is one writer. That is the scale story. Litestream is the documented
database RPO; /received and /outbound are file backups. See docs/deployment.md.
Browser tooling requires Node.js 20.19+, 22.13+, or 24+.
# server (needs Rust ≥ 1.97)
(cd server && cargo test) # unit + full-protocol integration tests
(cd server && cargo test --lib -- --ignored --exact session::push_tests::dormant_staging_preserves_ranges_and_cleans_up_under_low_descriptor_limit)
(cd server && cargo run) # needs VOTPORT_ADMIN_PASSWORD, VOTPORT_DATA_DIR, etc.
# browser JS
npm ci
for f in web/assets/*.js; do node --check "$f"; done
npm run lint
npm test
# browser wasm bundle (needs wasm32 target + wasm-bindgen-cli 0.2.126)
scripts/build-wasm.sh /path/to/VOT-checkoutThe descriptor-limit test runs alone because it changes the process limit. Spawning it from the parallel suite can briefly inherit another test's journal lock before exec, causing a parked staging file to refuse reopening. CI runs both commands.
The server pins vot-sdk / vot-sdk-file to an exact VOT commit in
server/Cargo.toml; the Dockerfile builds vot-wasm from the same commit so
browser and server always agree on the wire artifacts.
server/ axum server: admin API, upload protocol, VOT verify + publish
web/ static frontend: admin pages and the uploader (vot-wasm in browser)
scripts/ wasm build helper, login-disclosure tests
docs/ deployment, multi-tenancy design, enterprise-ops
POST /api/r/{link}/session announce package root (+ link password)
POST /api/session/{s}/seal manifest seal bytes
POST /api/session/{s}/page manifest pages, in order
POST /api/session/{s}/begin server verifies manifest, stages files
POST /api/session/{s}/chunk proof ‖ data for one 8 MiB range (repeat)
POST /api/session/{s}/finish all files verified → recorded
Each file is published the moment its coverage is complete, so a session that dies halfway still delivers the files that finished.
Native push is optional. It is enabled only when VOTPORT_PUSH_BIND is set;
the UDP port must be reachable from the sender and VOTPORT_PUSH_ADVERTISE
must name the address it can dial. The issuer key is always kept in
VOTPORT_DATA_DIR; an automatically generated listener certificate pair is
kept there too, while configured certificate and key paths are used in place.
/api/push-identity is public so a sender can obtain and pin the certificate
digest.
The sender first calls POST /api/r/{token}/push over HTTPS with the link
password (or its existing link cookie), holder public key, and the exact VOT
package root and length. The receiver checks the manifest entry count later
against its built-in MAX_ENTRIES limit. The response contains a one-session capability,
the advertised UDP address, certificate digest, and expiry. Pass the
capability and its holder key to the VOT CLI or equivalent VOT library:
CAPABILITY_B64='<capability from the preflight JSON>'
printf '%s' "$CAPABILITY_B64" | base64 -d > push-token.cbor
export VOT_PUSH_IDENTITY=<certificate_digest>
export HOLDER_SECRET='ed25519-secret:<64 hex characters for the holder secret>'
vot push BUNDLE_DIR <host>:<udp-port> push-token.cbor env:HOLDER_SECRETThe b14 CLI accepts a numeric IPv4 or bracketed IPv6 SocketAddr, not a DNS
hostname. Use a numeric address in VOTPORT_PUSH_ADVERTISE for that CLI; VOT
library callers may resolve names themselves. The link password never crosses
the UDP connection.
Native and browser uploads share link and tenant quotas, upload history, receipts, retention, and the admin UI. Browser uploads continue to use HTTP through the reverse proxy. Both paths write verified ranges directly to the receiving filesystem and publish each completed file without a payload copy. An interrupted transfer retains completed files and recoverable partials. See direct receiving and NAS qualification.
VOT is pinned at de66d41343cee7fb206a0f323d36babea655f032. Server, desktop core, and browser WASM use the same revision. HTTP and native push receive directly onto the destination filesystem, retain recoverable publication journals, and avoid a final payload copy. Linux SMB3 and NFSv4 require explicit storage qualification before Balanced receiving; Strict NAS receipts are unavailable. Open Storage > Receiving storage to check and enable the share. See direct receiving.
Native push remains disabled unless VOTPORT_PUSH_BIND is set. Browser uploads use HTTP through the reverse proxy, with bounded parallel range acceptance and the existing 8 MiB range ceiling.
Uploads reuse previously delivered content on the same request link after verifying the stored bytes. Per-link legal hold blocks upload retention and received-file deletion until the hold is released.
Not on the table: Postgres, a second store backend, horizontal replicas, SAML.
votport is self-contained (it consumes VOT only as pinned git dependencies),
so if it currently lives as votport/ inside the VOT repository you can
extract it with full history:
cd VOT
git subtree split --prefix=votport -b votport-standalone
# create an empty repo (e.g. halideworks/votport) on GitHub, then:
git push [email protected]:halideworks/votport.git votport-standalone:mainNothing in the project references its location inside the VOT tree.
Votport is proprietary commercial software. Use requires a separate paid written agreement from the repository owner. See LICENSE and contact the repository owner for commercial terms.
VOT remains separately available under its own license, but bundled VOT use with Votport follows the Votport commercial agreement. Third-party component licenses and notices are unaffected.