Repository navigation
Releases: JulesNsenda/drop
Release list
DROP 1.6.0
[1.6.0] - 2026-08-30
A hardening release, plus two things you can see.
Most of this closes work that earlier releases deliberately deferred with a
written reason rather than a TODO: container isolation's second tier, the
control plane's reachability from tenant containers, a backup that holds the
deploy pipeline still, and a production deploy that can put the previous release
back when the new one fails its health check. The two visible additions are a
read-only SQL console in the database panel and, for apps behind the access
gate, a page that retries itself instead of a blank tab while the platform
restarts.
Read the Upgrading section before you take this if you run
isolation: docker. Two of the container changes will stop an app that was
relying on behaviour that was never promised. On the default isolation: none
nothing breaks — you get one forced redeploy and the rest is additions.
Upgrading
- Every app is redeployed once on the first boot after this, under both
isolation modes. Four container-policy values (pinned image digests, the
read-only root filesystem, the tmpfs set, and the data-dir mount point) join
the fingerprint boot reconciliation compares, and that fingerprint is hashed
for every app rather than only containerised ones. Folding them in is the only
way any of them reaches an app that is already running — without it the
hardening would apply to new apps only, which looks like success on a fresh
box and like nothing at all on a real fleet. Expect one more redeploy each
time a base-image digest is refreshed. isolation: dockeronly — an app that hardcoded its data directory's host
path breaks. The persistent data directory now mounts inside the container
at/datainstead of at its own host path, so the host's directory layout is
no longer published to every tenant.DROP_DATA_DIRis rewritten to match, so
an app that reads the variable — the documented contract, and what the deploy
log tells every author to do — is unaffected.isolation: dockeronly — an app that writes outside/tmpand its data
directory breaks. The container root filesystem is read-only now./tmp,
/run,/var/runand/var/cache/nginxcome back as size-cappednoexec
tmpfs, which covers what language runtimes and nginx actually write.
Added
- A read-only SQL console in the database panel. Run a query against an
app's own database from the dashboard. Admin-only, and off until an admin
turns it on — not caution about a new feature: PostgreSQL's shared catalogs
are world-readable and no privilege setting closes them, so anyone who can run
arbitrary SQL can list every database and role on the server. Writes are
refused by the server rather than by inspecting the query text — every
statement runs in a read-only transaction, which is the only thing that stops
aSECURITY DEFINERfunction an app created from writing as its owner — and
the row cap is a server-side cursor. Each query is recorded in the activity
log: who and which app, never the SQL itself, since a query can contain the
very secret an audit trail exists to investigate. Newly provisioned databases
also get atemp_file_limitso one query cannot fill the shared disk; older
ones keep the timeout and memory bounds until reprovisioned. - Read-only container root filesystems, pinned base images, and a fixed data
mount. The Tier B isolation work that was deferred in June, for boxes that
will host untrusted tenants. Base images are pinned by index digest, so two
DROP installs pulling "the same" image on different days now run the same
bytes;DROP_DISABLE_IMAGE_PINNING=truefalls back to tags for an operator on
a private mirror. Seedocs/DOCKER-ISOLATION.md. - The control plane is closed to tenant containers. DROP's API is reachable
from the container bridge, because an app granted control-plane capabilities
calls it there. Every OTHER container could reach it too.DROP_API_HOST
makes the bind an operator decision (127.0.0.1is right for anyone with no
capability-holding apps), and a request arriving from the bridge is now
refused the credential-guessing endpoints, the credential-minting ones, the
browser-facing authorization flows and the dashboard. userns-remapis offered and reported.install.sh --userns-remap
enables it; opt-in, because on a running box it stops every container,
invalidates image storage, and changes which host UID a bind-mounted data
directory must be owned by. DROP now says at startup whether the daemon
actually provides it — without that, "Tier B shipped" reads as a guarantee the
platform cannot make on its own.drop backupholds the deploy pipeline still while it runs. Each write
was already atomic, but the file stores and the databases were not guaranteed
to describe the same moment — a deploy landing mid-backup could leave the app
state file from before it and the per-app config from after.--no-quiesce
opts out. The pause is a lease with a ceiling, so a backup killed mid-run
cannot leave the box refusing deploys.- The isolation mode is visible in Settings. The single biggest behavioural
switch on the platform was inferable only from the process environment or from
an app's symptoms. Reported read-only, with what to change and that a restart
is required — the app runtime is selected once at startup and cannot be
swapped while running. - Deploys can roll themselves back. The production deploy script is a file
in the repository now, shellchecked and smoke-tested in a container before it
ever runs for real, instead of a script embedded in a CI workflow where a
single apostrophe once truncated it mid-deploy and left the service stopped.
It snapshots the previous release first and restores it — code and
dependencies — if the new one fails its health check.scripts/deploy/rollback.sh
covers the case a health check cannot catch: a deploy that comes up fine and
is found bad afterwards. Seedocs/DEPLOY-ROLLBACK.md, which is explicit
about what a rollback does not undo.
Fixed
- Secret changes were never written to the audit log. The audit rules
described secrets at a URL this API has never served, so in practice no
request had ever matched them. Webhook, certificate-renewal and
database-panel activity is now recorded too — the surfaces where "which
credential did this?" is the first question after a leak. - An audit entry recorded whatever address the caller claimed. The audit log
kept its own copy of the client-address logic and read the first
X-Forwarded-Forentry, which is the value the client sent rather than the
one the reverse proxy appended. The same defect was fixed in the rate limiter
in 1.5.0 and missed here, so the one field a forensic record cannot afford to
take on trust was attacker-controlled.
Changed
- A
drop.yamlthat does not parse can now refuse the deploy. A malformed
manifest was discarded whole and silently: a typo in a secret name meant the
app declared no secrets, the preflight found nothing missing, and it started
unconfigured — which is what that preflight exists to prevent. Off by default
(DROP_STRICT_MANIFEST=true), because turning it on refuses to start any app
already carrying a malformed manifest, and the deploy log names them first. - A gated app now shows a retrying page, not a blank tab, while DROP
restarts. The access gate asks DROP's own API on every request, so a
platform deploy takes every gated app down for that window while ungated apps
keep serving. Measured, the visitor used to get an HTTP 502 with an empty body
and no content type; they now get a page saying the app is temporarily
unavailable, withRetry-After, that reloads itself and lands them back in
the app once DROP returns. It still fails closed — the page fires only on the
status codes an unreachable upstream produces, so a real refusal is untouched,
and it serves no tenant content. The underlying coupling is unchanged: plan
gated apps' maintenance windows around platform deploys.
DROP 1.5.2
[1.5.2] - 2026-08-30
A patch about what a deploy log tells you, and about output that was being
thrown away. Two reports drove it, and both diagnosed the platform from a log
that had omitted the one fact that would have corrected them: an app author
concluded DROP had no durable storage at all (it has always injected
DROP_DATA_DIR), and read a refused connection to port 5432 as a startup-order
bug (the bundled server has never listened there). The log now opens by naming
both. The rest is captured output that existed and could not be read back —
first-boot lines printed before the log follower attached, and a previous
deploy's output that vanished with its container. Nothing here changes
configuration; upgrading needs no action.
Changed
-
A deploy log now opens by saying where the app can write. Persistent
per-app storage has always existed —DROP_DATA_DIRis injected into every
app and bind-mounted read-write — but it was discoverable only by reading the
environment, and a failed write to the read-only source directory pointed at
the path rather than the mount. One app author concluded the platform had no
durable storage at all, shipped uploads to/tmp, and planned to take on S3.
Every deploy log now names the directory, itsDROP_DATA_DIRenv var, and the
fact that it survives redeploys; under Docker isolation it also says the source
directory is read-only. (#238) -
A deploy log now also names the database variables. The same report that
could not findDROP_DATA_DIRalso readECONNREFUSED 127.0.0.1:5432as
"DROP starts apps before Postgres is ready". Postgres was ready — DROP has
never listened on 5432 (the bundled server defaults to 5433 to avoid colliding
with a host install, and under Docker isolation apps reach it over a unix
socket rather than TCP at all), so an app building its own connection from
defaults was refused correctly and told nothing useful. The deploy log now
namesDATABASE_URLand thePG*variables up front. (#238)
Fixed
-
First-boot output is no longer lost under Docker isolation. The follower
that copies container output into DROP's own log files attached with
tail: 0, and it attaches after the container has already started — so
anything printed in that gap was never captured at all. A one-time admin
password printed on first boot is exactly what lands there. Measured against
Docker 28.3: a container printing a secret at startup, followed 700ms later,
missed it attail: 0and captured it attail: 'all'. It now follows from
container start. (#264) -
A deploy that outlives the MCP wait budget now returns its
deploy_id.
After ~120s the reply said only "still building, call app_status" — and since
get_deploy_logslooks a deploy up by that id and cannot disclose which ids
exist, whileapp_logsnever reads build output, a slow monorepo or cold
container build had no supported way to read its own output. The id was never
missing: the poll loop already read the correlated episode and discarded it at
the deadline. When no episode has correlated yet the message stays id-less
rather than naming a previous deploy's. (#265) -
A deploy's log output no longer dies with its container. Under Docker
isolation the logs API, the CLI, the dashboard and the MCPapp_logstool all
read the running container, so once a redeploy destroyed it the previous
deploy's output came back as an empty string — indistinguishable from an app
that had simply been quiet. When the container is gone the DROP-owned log
files are now read instead, with the same[out]/[err]prefixes the
dashboard's stream filter splits on. The container is still preferred while it
exists: it retains the opening window of the first run, which the file capture
can miss, and preserves the stdout/stderr interleaving two separate files
cannot. (#264)
DROP 1.5.1
[1.5.1] - 2026-08-29
A dashboard-only patch. One malformed API response could replace an entire page
with the error boundary at mount — not the panel that made the call, the whole
page, header and tabs and every action button with it. Nothing on the server
changed, and upgrading needs no configuration.
Fixed
- The app page white-screened on a degraded API response. When the secrets
endpoint returned an array instead of{ keys: [...] },data.keysresolved
toArray.prototype.keys— a function, so the?? []default never fired —
and React called it as a state updater, throwing out ofbasicStateReducer.
(#237) - The same array-versus-object assumption crashed six more pages. A
{}
payload passes every?? []and|| []default, and alength === 0guard
does not stop it either:{}.lengthisundefined, so the guard falls
through to.map()on an object. The apps list, the deploy timeline, the git
token list, the API keys tab, the activity log and the users page each went
down that way; the access tab and two filtered lists are hardened alongside
them. Verified against the real bundle: eight pages that failed on degraded
shapes before the fix all render after it. (#237)
DROP 1.5.0
[1.5.0] - 2026-08-28
Apps can now require a sign-in before anyone reaches them, owners can share an
app with a colleague, and a person with no DROP account can be invited to one
app by email. The dashboard was rebuilt on a real design-token system and is
usable from the keyboard throughout.
Everything in the sharing program is opt-in. appSharingEnabled defaults to
false, and an app with no policy behaves exactly as it did in 1.4.0 — upgrading
changes nothing until an admin turns something on.
Added
- App access gate. An app can be put behind DROP sign-in, enforced at the
proxy rather than by the app itself. The policy ismode: 'drop-users'with
an explicit allow-list; themodefield is the seam a future identity source
plugs into. - App sharing. An owner can grant a colleague access to one of their apps
without an admin round-trip, gated behind an admin-level toggle that is off by
default. - Outbound mail. An SMTP relay can be configured from Settings, with the
password encrypted at rest and write-only over the API. Share notifications
ride on it and ship default-off. - Guest access. Someone with no DROP account can be invited to exactly one
app by email and redeem a single-use invitation. Invitations expire, are rate
limited per principal, and the mail body only ever contains the operator's own
domain. - Dashboard: Overview and Activity tabs on the app page. The deploy history
was previously buried mid-scroll above the tab bar; it is now a destination of
its own, and the tab bar sits directly under the header rather than reading as
a footer. - Dashboard: per-row actions on the apps list. Restart, stop, start and
delete without opening the app first. - Dashboard: command palette. Cmd-K (Ctrl-K off macOS) jumps to any page or
any app. - Dashboard: a design-token bridge. Every
.drop-uitoken is a Tailwind
utility, with a lint guard so the hand-piped inline styles it replaces cannot
come back.
Changed
- The dashboard is keyboard-usable in places it previously was not: the confirm
dialog traps focus and restores it, the tab bar is one tab stop with arrow-key
navigation instead of one stop per tab, table headers are associated with
their columns, loading states announce themselves, and hints that were
mouse-onlytitleattributes now appear on focus. - Motion respects
prefers-reduced-motion, which the dashboard previously
ignored entirely.
Fixed
- Two high-severity advisories in transitive dependencies (
js-yamlquadratic
CPU consumption, reached via pm2 and ts-jest) and one moderate
(protobufjs, via dockerode).npm auditreports zero. - A credential-invalidation test that could fail on a fast CI runner when a key
was minted in the same millisecond as the suspension that should revoke it.
DROP 1.4.0
[1.4.0] - 2026-08-22
Backing services can now be attached to and detached from a running app, from
the dashboard or the API, and everything the platform can attach is listed in a
searchable catalog.
Detach destroys data, and the two services differ. Postgres is dumped on the
platform host before the database and its role are dropped; that dump is an
operator-side artifact with its own retention window, not a restore button in
the dashboard. Redis is flushed immediately with no backup at all. The
confirmation dialog states which of the two you are about to do.
Added
- Extension catalog.
GET /api/v1/extensionslists every backing service
and app type this build ships, with its availability on this instance
(installed, disabled by configuration, or unsupported under the current
isolation mode). A Catalog page in the dashboard makes the same list
searchable and filterable. Availability is platform-scoped: per-app facts,
such as whether a service is already attached or the owner's quota is full,
come fromGET /api/v1/db/<app>. - Attach a backing service.
POST /api/v1/apps/<app>/services/<id>(<id>
ispostgresorredis) provisions the service, records the choice, and
restarts the app with its URL injected. The response names the injected
variables but never their values. Available from an app's Database tab. - Detach a backing service.
DELETE /api/v1/apps/<app>/services/<id>
deprovisions it and restarts the app without the variable, so it is genuinely
gone rather than merely unused. - Attach and detach outrank
drop.yaml. The choice is recorded durably, so
a redeploy cannot quietly undo it: without that, the next deploy would re-read
database: postgres, or re-detect a Postgres client inpackage.json, and
provision a fresh database straight back. Resolution order is
attach/detach > drop.yaml > auto-detection. Once a service has been attached
or detached for an app, that manifest key stops deciding the question for it,
and re-attaching hands authority back. On an app deployed from a repository
you do not own, this is what stops a stale upstream manifest re-provisioning
against your quota. GET /api/v1/db/<app>also reports whether Redis is provisioned, the recorded
intent per service, per-service quota state, and whether the app is ephemeral.
A database recorded but missing on the server is now reported as a renderable
state rather than an error, so the tab still offers its repair controls.
Fixed
- An app with a DROP-managed database can now be repointed at an external
one. Setting aDATABASE_URLsecret was refused whenever a database was
provisioned, on the grounds that the injected URL would override the secret
anyway. With detach available that refusal became a lock-out: the data could
be gone and the owner still could not supply their own URL. The gate now
consults the recorded intent, so once a database is detached the secret is
accepted. The published documentation claimed a provisioned database "cannot
be handed back short of deleting the app", which detach makes untrue; it has
been corrected. pg_dumpis now bounded by a timeout and killed on expiry. A tenant holding
anACCESS EXCLUSIVElock on its own database could otherwise make a dump
wait forever, holding the platform's per-app lock until a restart.DROP_PREDELETE_RETENTION_DAYSno longer fails open. Any unparseable value
disabled pruning entirely and kept full database dumps, plus their plaintext
role-recreation files, forever.- A failed Redis
FLUSHDBno longer releases the logical database number. Two
tolerated Redis blips could previously hand one app's keys to the next
allocation. - Pre-delete database dumps are attributed to their owner by location rather
than by matching app names, so attribution survives the app's deletion and one
owner's cleanup can no longer evict another's only surviving dump.
Changed
- Platform-owned fields on an app's config (its recorded service intent, granted
API scopes, ephemeral flags) are now written through a separate path from
caller-supplied fields, and stripped from the general one. - CI actions are pinned to commit SHAs with Dependabot configured to propose
updates, build and test are defined once and shared by the CI and Deploy
workflows, and the deploy artifact's digest is verified before it is shipped.
DROP 1.3.0
[1.3.0] - 2026-08-14
A security fix in how depends_on reaches an app's environment, and support
for external databases through the encrypted secret store.
Prioritise this upgrade if you deploy a drop.yaml you did not write — a
third-party repository via deploy_from_git, or an agent-authored manifest. On
those paths the manifest author and the app owner are different people, and a
depends_on entry could overwrite any variable in the app's start environment,
the owner's own secrets included. A box that only ever deploys its operator's
own code was never exposed to the sharp half of this.
Security
-
depends_onindrop.yamlcould overwrite anything in an app's start
environment. Resolved dependency URLs were applied last, so adepends_on
entry could name any variable and silently win — a platform value such as
DROP_API_URL(redirecting where the app sends the scoped control-plane key
DROP mints for it), a provisionedDATABASE_URLorREDIS_URL, or any
secret the app's owner had set. The last of those is the sharpest: an
overwritten secret also satisfied the required-secret preflight, because that
check reads the merged environment — so instead of parking inneeds-config,
the app started with a session or signing key the manifest author chose.
This matters most when deploying a third-party repository, where the manifest
author and the app owner are not the same person.A
depends_onentry may now only fill a gap in the environment, never
overwrite an entry already in it. The collision is logged and that one
injection is skipped, so an unrelated attempt cannot fail an otherwise valid
deploy. The check compares against the environment actually assembled rather
than a list of protected names, so a variable added in future is covered
without anything to keep in sync.depends_on[].envvalues that are not
usable environment variable names are skipped the same way — skipped rather
than rejected during parsing, because a faileddrop.yamlvalidation
discards the entire manifest, which would both break already-deployed entries
and disable the required-secret preflight that depends on the discarded
secrets:block.
Added
- External databases can now be configured through the encrypted secret
store. Storing aDATABASE_URLsecret (Supabase, Neon, RDS, an external
MySQL) was refused outright, which left a plaintextenv:entry in the
tenant's owndrop.yamlas the only way to point an app at one. It is now
refused only when the app already has a DROP-managed database, whose
injected URL takes precedence over any secret and would override it anyway.
PGHOST,PGPORT,PGUSER,PGPASSWORDandPGDATABASEstay reserved,
alongsidePORT,NODE_ENVandDROP_DATA_DIR.
Fixed
database: truesilently discarded an app'senv:,secrets:and
services:. The validator requireddatabaseto be a string, so
database: truefailed validation and the whole manifest was dropped —
while a second, unvalidated reader went on provisioning the database, making
the loss easy to miss.databasenow accepts a boolean at the top level and
per service, matching what the platform actually acts on.- A monorepo service's
databasedeclaration survives materialisation. The
generated child config was written with a truthy test, sodatabase: false
was dropped entirely rather than passed down. Note thatdatabase: false
still does not decline a database — it falls through to auto-detection, as it
always has. Making it a real opt-out needs a deprovision story first, since an
app that already has a DROP-managed database cannot hand it back.
DROP 1.2.1
[1.2.1] - 2026-08-11
One security fix, and a correction to the note published with 1.2.0.
Upgrading is not enough on its own — restart your static apps. The nginx
config is regenerated from buildStartSpec, which runs on start and on
restart, so drop restart <app> (or the dashboard's Restart button, or the
MCP restart_app tool) applies the fix. A full redeploy is not required.
Before restarting, this tells you whether an app is exposed:
curl -o /dev/null -w '%{http_code}\n' https://<app>/.git/config # 200 = exposedAfter restarting it reads 404 either way, so it can no longer answer "was
I exposed, do I need to rotate a token?". That question is settled on the box,
and upgrading does not erase the evidence:
sudo grep -hE '^\s*url\s*=' /var/drop/data/webapps/*/.git/config | grep '@'Any app listed there was cloned with a personal access token in its remote
URL. If it is also a plain-root static app (outputDirectory is the app
root, not dist/build), that token was publicly fetchable — rotate it.
The 404 stops the bleeding but does not un-publish what was already fetched.
Security
-
A static app no longer serves its own dotfiles. The generated nginx
config now returns 404 for any path with a.-prefixed segment, at any
depth, with.well-known/carved out. For a plain-root static app the
document root is the app directory, sotry_files $urihappily served
/.git/config— the full repository history, and for an app cloned before
1.2.0 the personal access token baked into it — and/.env. Measured
againstnginx:alpinebefore and after:GET /.git/configreturned 200
with the token in the body, and now returns 404.This corrects the note published with 1.2.0, which named Caddy's
file_serveras the culprit. It is not:staticPathis never set, so that
branch is dead code. Static apps are served by nginx in their own container
and Caddy only reverse-proxies to it. The exposure was real, the component
named was wrong.An SPA is unaffected — its root is
/app/<outputDirectory>, so.git
sits outside the document root. That asymmetry is why this went unnoticed:
the SPA case, which is most apps, was always safe.Takes effect when the app's nginx config is next regenerated — a restart is
enough, see the upgrade note above.
DROP 1.2.0
⚠️ Correction — see 1.2.1The security section below says the static-app dotfile exposure is "not
fully closed by this change" and names Caddy'sfile_serveras the
component at fault. The attribution is wrong, and the exposure is now
fixed.Static apps are not served by Caddy's
file_server— that branch is dead
code, becausestaticPathis never assigned. They run nginx in their own
container, from a config DROP generates; Caddy only reverse-proxies to it.
The exposure itself was real: for a plain-root static app the document
root is the app directory, so/.git/configand/.envwere served.
An SPA was never affected.Fixed in 1.2.1.
Upgrading is not enough on its own — restart your static apps, which
regenerates their nginx config. If/.git/configreturned 200 on an app
that was git-deployed before 1.2.0, rotate the personal access token it
contained.
[1.2.0] - 2026-08-09
Two security fixes that change behaviour, and the recovery path for a repo
that goes private after it was deployed.
Read before upgrading — two compatibility breaks, both security-driven:
- An upload carrying
.gitis now refused.tar -czf app.tgz .from a
working tree fails instead of silently deploying the repository's git
metadata. Exclude it (--exclude .git), or deploy viaPOST /git/deploy. gitSource.tokenIdis no longer returned below the admin tier, and
gitSource.repoUrlis normalized. Breaking for any script or agent that
readtokenIdfromGET /api/v1/apps/:name.
Security
-
Uploaded archives containing
.gitmetadata are now rejected. An
archive with a.gitpath component — at any depth, case-insensitively —
is refused withreason: vcs_metadataand nothing is extracted. Previously
a tenant with theuserrole could upload a crafted.git/to
POST /apps/<app>/source, where it overwrote the app's real one, and a
subsequentPOST /git/redeploy/<app>rangit pullin that directory on
the host — never containerized in either isolation mode — so a poisoned
.git/config(anext::sh -c …remote URL,core.fsmonitor) executed
arbitrary commands as thedropuser, which is in thedockergroup and
therefore root-equivalent.The guard reads the parser's resolved entry path rather than the raw tar
header name, because a PAX extended header can override the path of the
entry that follows it — a check against the header name would have closed
nothing.Behaviour change for hand-rolled clients:
tar -czf app.tgz .from a
working tree now fails instead of silently deploying the repository's git
metadata. Exclude it (--exclude .git), or usePOST /git/deployto
deploy a repository. The MCPdeploy_filestool rejects such paths before
staging. -
Dotenv files are excluded from a dashboard folder upload.
.env,
.env.local,.env.productionand the like are skipped (templates —
.env.example,.env.sample,.env.template,.env.dist— still ship),
and the upload panel lists exactly what it left out. For a static app the
uploaded tree root is the web server's document root, so a shipped.env
was fetchable at/.envon the public URL. Use the secrets API
(PUT /api/v1/secrets/<app>) for values the app needs at runtime. -
Personal access tokens no longer reach disk on either git path
(DROP-142).git clone https://TOKEN@github.com/…recorded the URL
verbatim asremote.origin.url, so every app deployed from a private repo
carried its PAT in cleartext inside its own directory — which is the served
document root for a static app and is bind-mounted into the tenant's
container under docker isolation.gitPullwrote the same value for the
duration of a pull. Both now pass the token through a one-shot credential
helper that reads it from the git child's own environment (never argv,
which is world-readable viaps), scoped tohttps://github.comso a
tampered remote URL cannot redirect the credential to another host. -
Existing repositories are cleaned up on their next redeploy (DROP-142).
Closing the leak above does nothing for apps already on disk, and git
prefers a credential embedded in the remote URL — so on exactly those
apps the new helper would never fire. A redeploy now strips the userinfo
fromremote.origin.urlbefore pulling. Operator note: an app that is
never redeployed keeps the old value; grep for it with
sudo grep -hE '^\s*url\s*=' /var/drop/data/webapps/*/.git/config | grep '@'.Not fully closed by this change: Caddy's
file_serverhas nohide
directive, so a plain-root static app still serves/.git/config— and its
history — to the internet.
Added
- A git credential can be attached to an app that already exists
(DROP-142).POST /api/v1/git/redeploy/:nametakes an optional
{ "tokenId": … }: absent leaves the app's stored credential unchanged,
nullclears it, agit_…id attaches or replaces one. Answers "a repo
that was public and went private can no longer be updated" — the token was
always stored by reference and re-read at redeploy, but nothing could write
that reference after creation. The dashboard exposes it as a credential
picker next to the existing Redeploy button on an app's detail page.
Changed
gitSourceis narrower for non-admin API consumers (DROP-142).
GET /api/v1/apps/:nameno longer returnsgitSource.tokenIdbelow the
admin tier, andgitSource.repoUrlhas any userinfo stripped. The
gitSourcefield itself stays present at every tier. Potentially breaking
for a script or agent that readtokenIdfrom an app record.
Fixed
-
An archive whose entries the tar parser rejects no longer deploys as a
partial tree. node-tar runs non-strict here, so an entry with a bad
checksum was silently dropped while extraction still reported success — and
the destination was then pruned to match, deleting files that had gone
missing. Any parser warning is now fatal (reason: invalid_archive), except
the one node-tar emits for an archive with no entries at all, which still
reportsempty_archive.This closes the cases the parser reports. A tar stream truncated mid-way
inside an otherwise-valid gzip wrapper is still extracted up to the
truncation point, because node-tar signals nothing at all in that case —
measured, and not something a warning-based check can reach. -
Clearing a git credential actually clears it (DROP-142). A clear is
persisted before the pull rather than after it — otherwise the now
unauthenticated pull fails against a private repo and the clear is
discarded, leaving no way to detach a compromised token. -
Git operations are pinned to the app's own repository (DROP-142).
They ran with only a working directory, so an app whose.githad been
removed — by an upload deploy's prune, or a monorepo re-materialization —
resolved to whatever repository existed above it and reported that
repo's commit as the app's own. -
Container CPU is no longer under-reported by the host core count
(DROP-143). The core count was derived frompercpu_usage, a cgroup
v1-only field. Under cgroup v2 — the default on current Debian and Ubuntu —
Docker omits it and reportsonline_cpusinstead, so the divisor fell back
to 1 and every reading on the dashboard was the true figure divided by the
number of host cores. -
"Back to home" on the login and signup pages works again (DROP-145).
It linked to/, which since the site split resolves to the dashboard's
own host, redirects to/dashboard, and sends a logged-out visitor
straight back to/login— a closed loop. It now points at the marketing
host, and renders nothing at all on a single-host install, which has no
landing page to return to.
DROP 1.1.0
[1.1.0] - 2026-08-09
The marketing site moves out of the platform, a drop.yaml field that was
accepted but never read starts working, and four fixes land — every one of
them found by running the site split, not by reviewing it.
Added
port:indrop.yamlis now honoured. It has always been an
accepted, typed, validated field that nothing ever read, so an app that
declared a port still got an arbitrary one and had no way to find out. It
applies as a preference, never a claim: a port already held by another
app, one outside the configured range, or the DROP API port itself falls
through to normal allocation with a warning rather than failing the
deploy. Useful wherever something outside DROP hardcodes the upstream
port — a hand-written reverse-proxy host file, say. Check the app's
actual port after declaring one.
Changed
- The marketing site, docs and API reference now live in their own repo,
deployed as a separate app at dropkit.sh — the platform no longer builds
or serves/,/docsor/reference./now 301-redirects to
/dashboard; self-hosted installs that relied on the API-info JSON
previously returned at/will see a redirect instead. Release bundles
no longer contain adist/sitedirectory.
Fixed
/api/v1/healthno longer flaps between healthy and degraded. Under
Docker isolation the liveness probe was collecting live CPU and memory
for every container — roughly a second each — against its own 2000ms
budget. It emitted intermittent 503s on jitter while the runtime was
perfectly healthy, and degraded monotonically with every app added. The
probe now asks the runtime only whether it is reachable and how many apps
it manages; per-app stats are unchanged for the dashboard's own views.- Single-page apps deployed from source can be built under Docker
isolation at all. The build and the runtime shared one image choice, so
a static/SPA app was built innginx:alpine— which has no npm, so the
build could not succeed. Builds now select their own image and the app is
still served from nginx. - A tenant can no longer claim a hostname another app derives from its own
name: the reserved-host check saw only hostnames declared under
domains:, missing every implicit<name>.<suffix>. install.sh --provisionno longer overwrites a hand-edited apex Caddy
host file on each run. It creates the route only when absent, and refuses
to write through a symlink.
DROP 1.0.0
[1.0.0] - 2026-08-07
First public release. DROP now runs tenant apps under either PM2 or Docker
container isolation behind one runtime interface, exposes itself to coding
agents through a hosted MCP server with OAuth 2.1, and adds the guardrails,
monorepo support, and managed services needed to let an agent deploy safely
and unattended. This section also carries everything shipped but never
previously published since 0.1.0.
Security
- Access control: ownership enforcement on app mutation and log
endpoints (PUT /apps/:nameaccepts only a safe field allowlist — no more
userId/pathoverwrites); every deploy path (upload, git, agent
tooling) contains itself inside the webapps directory via a realpath
check that defeats symlink/junction/..escapes. - Multi-tenant isolation: a tenant-authored group or domain name can no
longer collide with, delete, or route-hijack another owner's app; a
colliding database name is refused instead of silently reused; deleting
an app now purges its logs and retained deploy artifacts instead of
leaving them readable by the next owner of that name; monorepo
materialization no longer lets one service's build escape into a
sibling's directory, and a dangling symlink can no longer squat a child
app's name. - Auth & API keys: authentication is on by default (
DROP_DISABLE_AUTH=true
to disable); JWT verification pinned to HS256; legacy password hashes are
compared in constant time and upgraded to scrypt on login;/auth/signup
is rate-limited; an API key's standing now derives from its owner instead
of the key being its own principal (which had let a suspended owner's
keys keep working, and orphaned apps/quotas); suspension and password
resets are reversible and contained rather than destructive; the
users:createcapability can no longer be escalated into arbitrary code
execution. - Agent & MCP surfaces: the untrusted-output fence around tenant-
controlled text — which stops a deployed app's text from acting as a
prompt injection against a model reading it — can no longer be forged or
bypassed; the MCPforward_authguard in front of a tenant's own MCP
endpoint now actually rejects every credential class except an
app-audienced bearer, instead of admitting others; per-app OAuth
audiences stop one app's token from reaching another app's MCP endpoint;
agent tokens carry an explicit scope grammar and are admitted narrowly at
the deploy gate. - Guardrails: closed several bypasses a dedicated security review found
in the agent-deploy guardrails — the circuit breaker and per-principal
quotas were inert on the exact code path an autonomous deploy loop rides,
and the idle reaper's dry-run budget was being spent by no-op sweeps
instead of real ones. - Build isolation: tenant build commands no longer inherit platform
secrets, on both the host and containerized build paths;install.shno
longer lets root execute drop-authored code, and the bundled Postgres
gets its own hardened, dedicated socket directory. - Webhooks: GitHub webhook signature verification no longer skips when
the header is omitted, guardsJSON.parse, and length-checks before
timingSafeEqual; outbound webhook URLs reject localhost/private/
link-local targets (SSRF). - Secrets: app secrets are encrypted with a standalone
encryption.key
(orDROP_MASTER_KEY) instead of a key derived from the store itself;
existing stores migrate transparently. - Misc: CORS defaults to same-origin (
DROP_CORS_ORIGINSto allowlist);
a Content-Security-Policy is set; git branch names are validated;
webhooks.jsonis0600; 500 responses no longer leak internal error
text.
Added
- Install from a published release.
install.sh --from-releasedownloads
the prebuilt bundle attached to a GitHub release, verifies its SHA-256 before
extracting anything, and installs without agit cloneor any TypeScript or
Vite build on the target machine. It requires an explicit--isolation=docker
or--isolation=noneon a first install, because that choice decides whether
tenant apps run in containers or as the system user that owns the platform's
encryption key — a one-line install command should not pick that silently.
Node.js, PostgreSQL and Caddy are provisioned for you; no C toolchain is
needed, since the last native dependency was removed. Every release also
attachesinstall.shand both checksums as individual assets, and the
landing page, documentation and README link them directly, so the bundle can
be fetched and inspected by hand before anything runs as root. - Docker isolation mode.
AppRuntimeis a formal seam with two
implementations — PM2 (isolation: none, the default) and Docker
containers (isolation: docker) — chosen once at boot from
config.isolation/DROP_ISOLATION. Container builds run in ephemeral,
non-root containers; static/SPA apps are served by an unprivileged nginx
with zero capabilities; Postgres has its own container-mode topology; live
stats and log streaming work the same way under both runtimes.drop migrate-runtimemoves an existing app between the two. - Hosted MCP server + OAuth 2.1.
POST /api/v1/mcp(PRD-040) exposes
DROP's own deploy/status/logs tools to Claude and other MCP clients.
OAuth 2.1 with PKCE (PRD-041) authorizes claude.ai's web connector,
including per-app MCP audiences and arevocation_endpoint. - Agent-deploy guardrails. A circuit breaker trips on a failing deploy
loop and resets on the first success; per-principal and per-owning-user
deploy quotas cap throughput regardless of outcome; ephemeral, TTL'd apps
(default 60 minutes, promotable to permanent) give agents a safe scratch
space; an idle reaper tears down abandoned agent-created apps; a per-app
disk ceiling blocks a deploy before it exhausts the box. Every limit
returns a structured refusal instead of a silent kill. - Monorepo / multi-service deploys. A
services:block indrop.yaml
expands one repository into N apps sharing a hostname, with
browser-reachabledepends_onURLs, same-origin/apirouting, and
group-aware start/stop/teardown/redeploy. - Managed Redis (PRD-050). A bundled Redis server provisions a per-app
logical database and injectsREDIS_URLfor apps that opt in via
drop.yaml. - Public site, docs, and reference — split from the dashboard (DROP-070).
/,/docs(PRD-043) and/reference(PRD-044) build as a separate
bundle from the authenticated/dashboardSPA, so a marketing visitor's
download never carries admin-only code. - Database panel. The dashboard's App Details page can browse an app's
provisioned database, reading it as the app's own database role rather
than an admin credential. - Multi-user MCP connectors. Non-admin users can set up their own
claude.ai connector; an admin-controlleduserConnectorsEnabledplatform
setting gates whether the capability is offered at all. - Agent-native deploy tooling: tarball upload deploys
(POST /apps/:name/source, PRD-039); scoped agent tokens
(POST /auth/agent-tokens) with a stable principal identity per caller;
a structured result for every deploy — a real error code, build-failure
classification from the log tail, andGET /deploys/:deployId/
get_deploy_logsto see why a specific deploy failed, with logs retained
past teardown. - Required secrets preflight (PRD-051). A deploy with
drop.yaml
secrets:missing is parked in aneeds-configstatus instead of
crash-looping, surfaced in both the API and dashboard. - Boot reconciliation. A platform restart no longer rebuilds every app
on the box; already-running apps are reconciled against their config
instead of redeployed. DROP_API_URL+ scopedDROP_API_KEY. Apps an admin grants
control-plane capabilities can call DROP's own REST API from inside their
own container/process with a least-privilege key, instead of needing the
admin key.- Auth: opt-in TOTP two-factor authentication; forced password change on
first login; admin-manageable GitHub webhook secret with a reveal-once
flow in the dashboard's Git settings tab. - CLI:
drop restorereversesdrop backup;drop backupnow also
captures every per-app database, not just platform state. - Dashboard: a log viewer (Runtime/Build tabs, stdout/stderr filter,
search with highlighting, pause/resume, copy/download, severity
color-coding, ANSI sanitization); settings reorganized into tabs (System /
Account / Activity / About) with the active tab kept in the URL; a
deploy-timeline panel and app-level Metrics tab (CPU/mem/uptime); a
redesigned auth flow, app shell, and design system; session-expiry
handling, a 404 page, logout redirect, an app-limit indicator
(GET /api/v1/usage), and a signup-success notice. - Continuous integration (GitHub Actions): lint, server build, tests, and
both dashboard builds on every PR tomain/develop. - Atomic, crash-safe writes (temp + fsync + rename) for every JSON/YAML
state store; a corrupt store is quarantined instead of silently wiped. .env.example, a LICENSE file, and afiles/prepublishOnlypackage
config.
Changed
drop serve -dnow applies the--root/--domain/--https/...flags it
forwards (previously ignored).- Boot recovery: apps whose process died while marked
runningare set to
pending(and restarted by the startup scan) instead ofstopped. - Version —
/health, the CLI,drop version, and the dashboard — is read
frompackage.jsoneverywhere, replacing several hardcoded, stale
version strings. - Dashboard assets are served with immutable cache headers;
index.htmlis
no-cache. - Git redeploy (API + webhook) always triggers a rebuild+restart after a
successful pull, including no-change pulls, and onboards a freshly cloned
app ...