All six planned phases are working: a Cloudflare-backed file spine, responsive dashboard, tags, hybrid search, static-site publishing, deployment-based authentication, and scheduled storage lifecycle management. Uploads stream directly to R2, metadata and append-only version history live in D1, and file/site bytes are served from a separate cookie-less content origin. Search combines weighted FTS5 BM25 results, a filename trigram index, and an optional Workers AI + Vectorize semantic source with reciprocal rank fusion. Canonical D1 hydration still applies deletion, expiry, visibility, and tag filters. The CLI supports file transfer and safe, staged directory publishing.
Single-file build, no repo checkout needed (requires Node 20+):
curl -fsSL https://raw.githubusercontent.com/davis7dotsh/aDrive/main/scripts/install-cli.sh | bashThis installs to ~/.adrive/bin (no sudo), verifies the download against
the release's SHA-256 checksums, and prints PATH guidance. Pin a version
with ADRIVE_CLI_VERSION=cli-v0.1.0, update later with adrive upgrade,
then connect with adrive login <your-drive-url>.
Requirements: Node 26+ and pnpm 11+.
pnpm install
pnpm db:migrate:local
cp apps/web/.dev.vars.example apps/web/.dev.vars
pnpm key:create:localReplace the example PASSCODE with a long local-only value. Copy the API key
printed by the final command, then start both local origins:
pnpm --filter @adrive/web devThe dashboard/API is at http://localhost:5173/. Public file bytes are
served from http://localhost:5174/. The second port is a small streaming
proxy into the same SvelteKit process so both origins share one local D1/R2
state while the Worker still sees and enforces the content host.
The dev server binds 0.0.0.0, so other devices can use it — phones,
tablets, or a laptop pointed at a beefier dev box. Set both origins in
apps/web/.dev.vars to the hostname the browser will use. With
Tailscale MagicDNS that's your machine name plus tailnet domain (see
tailscale status):
DASHBOARD_ORIGIN="http://<machine>.<tailnet>.ts.net:5173"
CONTENT_ORIGIN="http://<machine>.<tailnet>.ts.net:5174"Any LAN hostname or IP works the same way; the origins just have to match how the browser addresses the machine, since the Worker enforces its host-routing rules even in dev.
Production passcode login creates a seven-day, host-only
__Host-adrive-session cookie. That cookie is always Secure, HttpOnly, and
SameSite=Strict, so browsers correctly refuse it on plain-HTTP non-localhost
dev URLs (like a Tailscale hostname). Use the generated API key in the
dashboard's “Local HTTP fallback” there. Passcode login works on
http://localhost and on any HTTPS dashboard origin.
Authentication bootstrap endpoints are protected by the AUTH_GUARD Workers
KV namespace. Login is limited to ten attempts per client every five minutes,
and five incorrect passcodes within fifteen minutes lock that client out for
thirty minutes. Device authorization creation is limited to five requests per
client every ten minutes, while device polling permits 150 requests per client
every ten minutes so the documented five-second polling interval remains valid.
Rate-limit and lockout responses use HTTP 429 with Retry-After. KV is
eventually consistent, so these controls are abuse mitigation rather than an
atomic global security boundary.
In another shell, configure and exercise the CLI:
pnpm adrive login http://localhost:5173 --headless
pnpm adrive list
pnpm adrive put ./path/to/file.pdf
pnpm adrive put ./path/to/private.bin --private
printf 'from stdin' | pnpm adrive put - --name note.txt
pnpm adrive put ./report.csv --expires 2026-08-31T00:00:00Z
pnpm adrive get <file-uuid> --output ./downloaded-file
pnpm adrive get <file-uuid> --output - > downloaded-file
pnpm adrive site put ./dist
pnpm adrive site put ./dist --id <existing-site-uuid>
pnpm adrive --json tag list
pnpm adrive tag create reports --color '#2563eb'
pnpm adrive tag set <file-uuid> reports importantlogin starts a device flow. Normal mode tries to open the approval URL;
--headless prints the same complete URL so it can be opened on another
machine. Approval mints one full-access API key and saves it at mode 0600
under $XDG_CONFIG_HOME/adrive/config.json (or
~/.config/adrive/config.json). Device codes are stored only as SHA-256 hashes
and expire after ten minutes.
Uploads default public; HTML is always made public. Human-mode upload and site
success output keeps the public URL alone on its own line. Put --json before a
command for JSON stdout. Downloading with --output - writes only file bytes to
stdout and cannot be combined with --json; diagnostics remain on stderr. The
CLI first requests a typed content link from the authenticated dashboard API,
then downloads directly from the cookie-less content origin without forwarding
its API key. Public links are stable. Private file links are scoped to one exact
version, signed with a deployment-only HMAC, and expire after 15 minutes; the
dashboard clearly labels these expiring links when copying them.
site put walks regular files without following symlinks, declares the complete
manifest, streams assets with four uploads at a time, and atomically publishes
only after every asset is present. A republish records a new audit version,
switches the stable /s/<uuid>/ URL to it, and removes the prior R2 asset set.
Site versions are intentionally not addressable with ?v=.
These commands do not start or build the app:
pnpm format:check
pnpm check
pnpm testRun pnpm --filter @adrive/web types:worker after changing Wrangler bindings.
The checked-in worker-configuration.d.ts is generated from wrangler.jsonc.
The Cloudflare adapter wrapper emits a standard module Worker with both fetch
and scheduled exports. Its signed internal maintenance request is authenticated
with a short-lived HMAC derived from PASSCODE; the endpoint cannot be invoked
with a static or public header.
The FTS5 tables are derived state so canonical tables can be backed up one table at a time even though D1 whole-database export does not support virtual tables. After restoring the canonical tables, rebuild local search state with:
pnpm search:rebuild:localThe rebuild reads files, file_versions, tags, and file_tags; it does not
modify those source tables.
The checked-in Wrangler D1 and R2 resource names are placeholders. The
AUTH_GUARD KV namespace is already provisioned and bound. Before deployment,
create one D1 database and one private R2 bucket, replace the D1 database ID,
apply the migration remotely, set production dashboard/content origins, and set
the passcode as a secret:
pnpm --filter @adrive/web exec wrangler secret put PASSCODENo remote resource is created or modified by the local setup above.
File expiration is enforced immediately by API, search, file, and site reads.
The scheduled Worker runs every five minutes. It re-drives interrupted indexing,
physically deletes expired/trash bytes before removing canonical D1 rows, expires
dashboard/device/site-upload sessions, and retries deferred R2/site/vector
deletes. Work is bounded per invocation and every retryable transition is stored
in D1. Download counts increment for full downloads and the initial
bytes=0-… request only, so follow-up range requests do not inflate the count.
Keyword, typo-tolerant, tag, and extracted-text search work without AI bindings.
SEMANTIC_SEARCH defaults to auto: both AI and VECTORIZE must be present
before the semantic layer activates. Set it to off to force the null-object
layer, or required to make a missing binding a startup error.
Provision the optional resources once, before the first vector insert:
pnpm --filter @adrive/web exec wrangler vectorize create adrive \
--dimensions=384 --metric=cosine
pnpm --filter @adrive/web exec wrangler vectorize create-metadata-index adrive \
--propertyName=deleted --type=boolean
pnpm --filter @adrive/web exec wrangler vectorize create-metadata-index adrive \
--propertyName=kind --type=string
pnpm --filter @adrive/web exec wrangler vectorize create-metadata-index adrive \
--propertyName=visibility --type=stringThen add these bindings to apps/web/wrangler.jsonc and regenerate types:
pnpm --filter @adrive/web types:workerThe embedding contract is pinned in Wrangler config to
@cf/baai/bge-small-en-v1.5, pooling: "cls", and 384 dimensions. Changing any
of those values requires a new Vectorize index and a full reindex. The dashboard
shows the indexed chunk count and warns that Vectorize queries are billed against
stored vectors multiplied by dimensions. Failed files retry with exponential
backoff up to five attempts and can be queued again with Reindex.