nnesly

Changelog

What shipped, newest first. Anchored to the public releases where one exists.

You can tell us when a site we host is being used to harm people

  • There is a report form at /abuse. No account needed, and you do not have to give your name — the person reporting a phishing page is usually not our customer.
  • Every report gets a reference. Keep it if you write to us about the same thing again.
  • We read them all. What happens next depends on what it is: we may contact the customer, take the site down, or both. Taking a site down stays a decision a person makes, not something a form triggers.
  • We do not e-mail anyone from that form, including you. An address typed into a public form is not a verified one, and mailing it would make the form a way to send mail in our name.

Your visitors can sign in with your own Google, Microsoft, OIDC or GitHub app

  • nesly auth provider set --kind oidc --issuer https://accounts.google.com --client-id ... --client-secret ... puts your own identity provider in front of your site. Your visitors sign in with it; your application writes no authentication code and reads X-Nesly-User-Id / Email / Name from the request.
  • --kind github does the same with a GitHub OAuth app. Any OpenID Connect issuer works with --kind oidc: Google, Microsoft Entra, Auth0, Keycloak, Okta.
  • You register the application with your provider and give us its client id and secret. We have no shared Google or GitHub app to lend you — a shared one would put our name on your consent screen.
  • nesly auth provider show prints the one redirect URI to register. One, for every address the project answers on: you register it once and it keeps working when you add a domain.
  • Who may pass is still yours to decide with nesly auth allow. If you name nobody, the gate admits everyone that provider can authenticate — so the command asks you to say that on purpose before it will do it.
  • Changing or removing the provider ends the sessions opened through the old one.
  • Agents: MCP get_auth_provider, set_auth_provider and clear_auth_provider on the default surface.

Sign-in protection now asks who you are, not just whether you signed in

  • nesly auth protect used to let through anyone with an account here. It now admits only people who can reach the project: your team, anyone invited to the project, and the addresses you name.
  • nesly auth allow ada@client.com lets somebody outside your team in; @client.com allows everyone at a domain. nesly auth allow with no arguments prints the list, --clear goes back to your team only.
  • nesly auth status now says who may pass, not only whether the gate is on. The panel shows and edits the same list.
  • An address is matched only after we have confirmed it belongs to its owner, so nobody reaches your site by registering with somebody else's address.
  • If you had the gate on and were relying on any account of ours getting through, add those addresses with auth allow. We measured before shipping: no project had the gate on, so this changes nothing that was working.
  • Agents: MCP set_auth_allowlist on the default surface, and set_auth_protection / get_protection now report who may pass.

Operator notices on the CLI

v0.18.11
  • nesly operator notices publish|ls|resolve lets operators post and close shared-engine notices from the terminal (same as the operator console). Tenants still use nesly status.
  • Upgrade with brew upgrade --cask nesly.

Platform notices on the CLI

v0.18.10
  • nesly status lists active platform notices (shared database or engine maintenance). Empty means none — this is not project maintenance.
  • Agents: MCP get_platform_status on the default surface (requires a rolled mcpd).
  • Upgrade with brew upgrade --cask nesly.

Agents can pull a local .env on the default MCP surface

  • MCP pull_env is available without switching to the full tool surface — same derived database variables as nesly env pull, values written only to the path you pass (not returned in the tool response). Restart the agent MCP session after mcpd rolls.

Predictable preview URLs with --alias

v0.18.9
  • nesly deploy --preview --alias staging publishes at {project}--staging on your platform domain instead of a random deployment id. Redeploying the same alias replaces the previous preview at that address.
  • Agents: MCP deploy_project with environment=preview and alias.
  • Upgrade with brew upgrade --cask nesly.

Agents can delete environment variables on the default MCP surface

  • MCP delete_env is available without switching to the full tool surface (alongside set_env, list_env, and clone_env). Restart the agent MCP session after mcpd rolls.

Promote a static preview without re-uploading

v0.18.8
  • nesly promote <deployment-id> now works for static sites: production reuses the same published files (no upload, no rebuild). Containers still reuse the image.
  • Agents: MCP promote_deployment follows the same rule.
  • Upgrade with brew upgrade --cask nesly.

Publish a finished static site without a platform build

v0.18.7
  • nesly deploy --prebuilt --dir dist uploads your built site and publishes it as a static deployment — no Nixpacks run, no build minutes. Use after your own vite build / hugo / similar.
  • Agents: MCP deploy_project with prebuilt=true and dir (incompatible with image).
  • Upgrade with brew upgrade --cask nesly.

Promote a validated release without rebuilding

v0.18.6
  • nesly promote <deployment-id> ships that release's container image to production (no upload, no rebuild). Use it after a preview you checked. Static sites still redeploy.
  • Agents: MCP promote_deployment on the default (core) surface.
  • Upgrade with brew upgrade --cask nesly.

Sign in from the CLI without a typed code

v0.18.5
  • nesly login in a browser on the same machine no longer asks for a one-time code after MFA — the session hands off automatically. Headless --no-browser still requires the code.
  • Success text no longer prints a credentials file path (the session stays in the system credential store).
  • nesly env clone --from <project> copies stored environment variables to another project without printing values.
  • Upgrade with brew upgrade --cask nesly.

Require platform sign-in before visitors reach your site

v0.18.4
  • nesly auth protect turns on edge sign-in for a project (off by default). Visitors sign in on the console; your app receives X-Nesly-User-Id, Email, and Name without its own login code. Clear with nesly auth clear. This is not the OIDC addon and not preview password protection.
  • Upgrade with brew upgrade --cask nesly.

Safer whoami for agents, and clearer CLI wording

v0.18.3
  • nesly whoami --json returns signed_in: false (exit 0) when the API is unreachable or the session expired, so agents can branch without treating dial noise as a hard failure.
  • nesly domain buy requires --yes. nesly env pull lists production variables.
  • CLI help and diagnose wording follow Azure product verbs (Create, supporting log lines). Upgrade with brew upgrade --cask nesly.

Skip a GitHub deploy from the commit message

  • Put [skip ci], [ci skip], [no ci], [skip actions], or [skip deploy] in a commit message or pull request title and that push or PR update will not start a build. The token can be on any line of the commit message.

Rotate addon credentials without deleting data

  • nesly addon rotate <type> --yes (and MCP rotate_addon) re-issues the password, access key, or client secret for an attached addon and rewrites its env vars. Database contents and object data stay. Redeploy or restart so the app picks up the new values; old credentials stop working.

Cancel a build, redeploy, and private image registries

v0.18.2
  • nesly cancel stops a queued or in-progress build. nesly redeploy starts a new build from the same source as an existing deployment.
  • nesly registry set|ls|rm stores pull credentials for private third-party registries (for example ghcr.io) used by deploy --image. Passwords are sealed at rest and never listed.
  • CLI v0.18.2 is on Homebrew (brew upgrade --cask nesly).

Outbound IPs for bank allow lists

v0.18.1
  • nesly egress (and Account in the console) lists the shared source addresses your apps use on the public internet — what banks and payment gateways ask you to allow. The list is platform-wide, not a dedicated IP per project.
  • CLI v0.18.1 is on Homebrew (brew upgrade --cask nesly).

DNS you manage here, and a calmer console

v0.18.0
  • After you point a domain's nameservers here (nesly domain delegate + verify), nesly dns ls, add and rm edit A, AAAA, CNAME, TXT and MX records in that zone. Platform records that keep the site and ownership proof online cannot be removed.
  • CLI v0.18.0 is on Homebrew (brew upgrade --cask nesly). Refresh races between the CLI and MCP no longer wipe your session.
  • The console no longer shows raw webhook kinds, English certificate probe text, or IdP error jargon; live logs follow the active deployment instead of an arbitrary older one; lost-device MFA reset asks for your password only.

ASP.NET schema migrations actually run before the first visitor

  • release = "dotnet ef database update" in nesly.toml now works on .NET apps. Until today that line was documented and the connection string reached the release job — but the runtime image has no SDK and no dotnet-ef tool, so the command could not succeed.
  • When your repository has a Migrations folder, the build produces a migration bundle at /app/efbundle. The platform rewrites the documented release line to that binary and passes ConnectionStrings__Default (or DATABASE_URL).
  • No Migrations folder and that release line together are rejected at build time, with a sentence naming what to add — not a cryptic failure after deploy.

Object storage you attach like a database

  • nesly addon add bucket gives your project its own S3-compatible bucket in Turkey. AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_ENDPOINT_URL and AWS_BUCKET are injected — the names Laravel, boto3 and the AWS SDKs already look for.
  • Your login can only read and write your bucket. Another project's bucket returns Access Denied, and the platform's own storage stays out of reach.
  • Files no longer have to leave for a foreign S3 and a dollar invoice. Durable structured data still belongs in postgres, mysql or mssql; sessions and short caches still belong in valkey.

A key-value store you attach like a database

  • nesly addon add valkey gives your project a Redis-compatible store. REDIS_URL and VALKEY_URL are injected — the two names frameworks actually look for — and both point at the same login.
  • You get your own user and a key prefix. Another project's keys are unreachable, and nobody can flush the shared server. When the server is full, writes fail loudly instead of silently deleting someone else's data.
  • Use it for sessions, rate limits, short-lived caches and queues. Durable data still belongs in postgres, mysql or mssql.

Environment variables now say which environment they belong to

  • ⛔ If you gave a preview its own value for a name your production already used, the console showed the same name twice, identically, with nothing to tell the two apart. Worse, a list with two identical names drops one of them, and which one disappeared was a coin toss — sometimes the production credential.
  • Every variable now carries the environment it applies to, and only the scoped ones are labelled: a variable that applies everywhere stays quiet, because saying it on every row would bury the handful that matter.
  • Three views: all your variables with their scopes, or what production actually sees, or what preview actually sees. The last two include the values inherited from "all environments", so it is the list your application really gets.
  • Adding a variable while looking at one environment writes it to that environment. Removing one removes it from its own environment — deleting a preview override no longer takes your production value with it.
  • This was already possible from the command line. It was the console that could not say it.

See how much memory your application uses — and ask for more

  • nesly ps now shows what each process is using against what it is allowed to use, for example 221Mi of 512Mi. Until now nothing on the platform answered the question a restarting process actually raises, which is not "how much memory" but "how close am I to being killed for it".
  • ⛔ And until now you could not move that ceiling at all. Every application got the same 512Mi, so an application that needed more simply could not run here — it was killed, and the log said "Killed".
  • Write it in your nesly.toml and it travels with your code: a [resources] section with memory = "1Gi". Rolling back to an earlier version restores the size that version was written for, which is the point of keeping it next to the code.
  • Asking for more than your plan allows stops the deploy and tells you both numbers and which line to change. We would rather stop you there than hand your application less memory than it asked for and let it die later for a reason that looks unrelated.
  • It is not a new charge. Usage was already measured from the ceiling, so a larger one costs more on the bill you already have.
  • A process that has not been measured yet shows a dash, never a zero: "using nothing" and "nobody looked" are different answers and only one of them is good news.

Interactive sessions end when they end

  • ⛔ nesly run --tty printed everything your command produced and then hung for another eighty seconds before giving up with an error, on a session that had finished successfully on our side in under two. Scripts saw a non-zero exit for work that had actually worked.
  • The cause was ours and it was at the edge: the end of the call was waiting for your keyboard to close, and a terminal never closes. The same session run directly against the control plane always took two seconds.
  • The same command now finishes in twelve seconds and exits zero.

See the known vulnerabilities in what you deployed

  • Every image built here has been scanned since day one. ⛔ Nothing showed you the result — it was produced, stored, and shown to nobody. Scanning your application and keeping the answer is the worst of the three options, and it was the one we were doing.
  • nesly scan reports what the scanner found in the image your deployment runs: the package, the version you have, and the version that fixes it, with the worst findings first and the fixable ones above the ones nobody can fix yet.
  • nesly inspect now says how many there are, so you find out without having to know the command exists.
  • --json carries the whole list, so a pipeline can read it. The screen shows the first twenty and says how many it kept back.
  • Nothing is blocked. A finding does not stop a deployment today, and if that ever changes we will say so before it does, with the threshold written down.
  • A static site answers "there is no image to scan" rather than "no findings" — those are different statements and only one of them is true.

Deploy .NET 10 — and whichever version your project actually targets

  • ⛔ Only .NET 8 built. A project targeting net9.0 or net10.0 failed with undefined variable 'dotnet-sdk_10' — a message that names neither your project nor anything you can do about it. .NET 10 has been the LTS since November 2025, so a fresh dotnet new produced exactly the case that did not work.
  • Your <TargetFramework> now decides the build. net8.0, net9.0, net10.0 and whatever comes next: we build against the official Microsoft SDK image for that version, which exists the day it ships.
  • A .csproj we cannot read the framework from is rejected up front, in a sentence, instead of failing several minutes later inside a build log.
  • Nothing changes for Node, Python, Go or Ruby projects.
  • This one is on us: .NET is the reason this platform exists, and it was the stack that did not work in its default shape.

Domain search was telling you the opposite of the truth

  • ⛔ nesly domain search reported registered domains as available to buy, with a price, and available ones as taken. Every line could be wrong, and there was no way to tell from the output.
  • The cause was ours: the safety switch that keeps domain purchases off real money while card payments are being finished was also routing the availability question to a test registry, which answers about a made-up world.
  • Fixed: availability is always asked of the real registrar. The purchase lock is untouched — buying a domain still waits for card payments.
  • Nobody could have bought a wrong domain because of this, since purchases are not open yet. But the prices and the answers you saw were not real, and we are saying so.

Bring your domain's DNS here and stop editing records by hand

  • nesly domain delegate example.com opens a DNS zone for your domain on the platform and prints two nameservers. Set those at your registrar and that is the last DNS work you do for it.
  • Until now, adding a domain meant being handed a CNAME and a TXT record and being sent to a DNS panel — which assumes you know what a CNAME is and have somewhere to put one. Root domains made it worse: the specification forbids a CNAME there, so you got A records instead and had to know why.
  • Once the nameservers point here, nesly domain verify finishes the setup and writes the records itself. You create nothing.
  • Run domain delegate again at any time to see where the change stands — it prints the same nameservers and when we last checked, because registrars can take a few hours to publish and silence is not an answer.
  • ⛔ A half-finished move does not count as done. If your registrar publishes one of ours next to one of your old ones, we say not yet rather than writing records into a zone the internet only sometimes asks.
  • Nothing changes for domains you already added: their DNS stays wherever you keep it and they verify exactly as before.
  • This is also the groundwork for managing the rest of the zone — mail records and verification records for other services — from the same place. That comes next.

Run your migration between the build and the first visitor

  • A [deploy] block in nesly.toml names one command to run once, before the new release serves anything: release = "dotnet ef database update". Add timeout = "10m" to bound it (15 minutes by default, 30 at most).
  • Until now the only way to migrate a schema was nesly run -- npm run migrate, which has no ordering relationship to the deploy. Run it after deploying and the new code was already live against the old schema; run it before and the old code met the new one. Neither order is safe, nothing warned about either, and in a pipeline the second command failing left the first one already shipped.
  • It runs in the image that was just built, with the environment variables that deployment will run with — so a preview migrates the preview's database, never production's.
  • If the command exits non-zero the deployment stops there: no instance is ever started and your previous release keeps serving. Its output goes to the deploy log, which is where you will look.
  • ⛔ Rolling back does not run it again. A rollback returns to an older image, and running that version's migration would move your schema forward rather than back — so a rollback stays immediate and predictable.
  • Most projects need no such block and pay nothing for it.

Four things you wrote in nesly.toml now actually reach your deployment

  • ⛔ Correction, and an apology: [[redirect]], [[header]], [[rewrite]] and [health] were announced and did not work. Your configuration file was read and those four blocks were resolved correctly — and then dropped on the way to your deployment. Nothing failed and nothing was logged, so a redirect you wrote simply never happened.
  • The cause was ours and dull: a build result reached the platform through two different paths, and the one every real build takes could not carry those four fields.
  • Fixed, and the fix is structural rather than a patch: there is now one conversion instead of two, with a check that fails our build if a field cannot make the trip. The first field added after it was caught immediately.
  • What you need to do: redeploy. Deployments built before this carry no such rules — that is the honest state of them, they ran without those rules — and a new deploy picks them up.

Block a request for what it is asking for

  • nesly protect rule block --path /wp-admin/* blocks a path. --user-agent BadScraper blocks a client by name. --header "X-Api-Key: leaked" blocks one header value. nesly protect rule ls shows them in the order they apply.
  • The three arms that existed before this all answered "who is asking": a rate limit slows one address down, a country rule blocks a region, an address rule blocks one address. None of them could say anything about the request — so a flood aimed at one path, or one scraper signature hitting every page, could only be answered by closing your site to a country or an address, which costs you real visitors.
  • Rules are ordered and the first match answers. A request matching none of them is served, and so is every request on a project with no rules.
  • ⛔ Block only. An allow list written over paths would close every path its author did not think of, and one mistyped rule would take a site down — which is also why a rule matching every request is rejected when you write it. Closing your site on purpose is nesly maintenance on, which says so on the page and comes back in one command.
  • A blocked visitor sees a plain page that does not name your rule, your path or your list. Someone probing for what is blocked would read a precise message as a map.
  • This is the one protection that works everywhere: a path, a header and a client name all travel in the request, so unlike the address rule it does not need the edge to see the visitor's own address.

Say what healthy means, and a broken release stops instead of going live

  • A [health] block in nesly.toml names the path we ask before pointing your address at a new release, and optionally the status it must answer: path = "/healthz", status = 200.
  • ⛔ Without one, any answer counted. An application that starts, cannot reach its database and answers 500 to everything was declared ready and took every visitor — the process was alive, so nothing measured anything as wrong. With a health check it stops instead, and your previous release keeps serving.
  • The same path becomes the readiness check for every instance afterwards, so one that stops answering stops receiving visitors. Write the check to test what your application actually needs; that the process is up is something we can already see.
  • Without a status, anything below 500 passes. A 404 counts as healthy because an application that answers 404 is running — which path it serves is your business, not ours.
  • Nothing changes for projects that do not add the block: the root path is asked and any answer is accepted, exactly as before. We were not willing to fail every API-only site whose root is a 404 overnight. A release that answers with a server error under that rule now says so in the deploy log rather than going live quietly.
  • The check travels with the deployment, so rolling back restores the release and the question asked of it together.

Set a secret without typing it where everyone can see it

v0.13.0
  • nesly env set DATABASE_URL with no value reads it from standard input, so you can pipe it in or paste it without it ever becoming a command-line argument. nesly env set --from-file .env imports a whole file at once.
  • ⛔ This closes something we were doing wrong. A value written as an argument is readable by anyone who can list processes on the machine, and your shell writes it into its history file — so setting a database password used to leak it twice. Our own help text gave that as the example.
  • The file format is the ordinary one: KEY=VALUE lines, comments and blank lines skipped, one layer of quotes removed. Nothing is expanded or substituted, so what gets stored does not depend on where you read it. Forty variables from another platform are now one command instead of forty.
  • nesly env set KEY=value still works exactly as before, for the values that are not secret.

Let an assistant deploy for you, in one command

  • nesly mcp setup connects the AI assistants installed on your machine — Claude Code, Cursor — so you can ask one to deploy, read logs or explain a failed build without leaving it.
  • Before this you had to know your assistant's own configuration commands, which is a strange thing to require of a user whose next step is nesly deploy.
  • It does not ask you anything, because this command line never prompts. Run it and it reports what it connected. Run it again and it does nothing.
  • Your assistant's configuration file holds its other servers and settings; we read it, add one entry, and write everything else back as it was. A file we cannot read is left alone rather than replaced.
  • nesly login prints the line once, when nothing is connected yet.

Every request has a name now

v0.12.0
  • Every response your site sends carries an X-Nesly-Request-Id header, and the same value reaches your application as a request header. nesly logs --request req_abc123 finds the lines belonging to that one request, in stored logs and in a live stream.
  • This is for the support conversation that used to end in a shrug. Somebody sends you a screenshot of a broken page, and all you had was a timestamp and a guess about which of the day's requests it was.
  • If your load balancer or CDN already puts an id on the request, we keep it, so one request has one name across both. We only accept a value that is short and made of letters, digits, dashes and underscores — anything else gets a fresh id rather than being passed along.
  • ⛔ This is not distributed tracing, and we are not going to pretend it is. There are no spans, no waterfall and no trace store: those are a different product and a different shape of system, and we have not decided to build one. What we can do is give every request a name and let you search on it.
  • The honest limit: we cannot join the two halves for you. Your application has to print the id in its own log lines for --request to find them. The header arrives on every request; printing it is one line in your logging setup.

Serve part of your site from somewhere else, under your own address

  • A [[rewrite]] block in nesly.toml serves a path from another origin without the visitor leaving your domain. from = "/blog/*" and to = "https://blog.example.com/:splat" puts a blog you host elsewhere under your own address — no redirect, no second request, no address bar change.
  • Until now the file could only redirect. That is a different thing and visitors can tell: the address changes, the round trip doubles, and anything that cared about your domain stops caring.
  • It is the same matching language as redirects, including :splat, because you should not have to learn two. The rules travel with the deployment, so rolling back a release rolls back its rewrites with it.
  • ⛔ We check where a rewrite points, twice. When you write the rule we reject anything that is not https, any bare address, and any name that only exists inside our cluster. When the request actually happens we check the address again at the moment of connecting, using the same rule that guards webhooks and log drains — not a copy of it, the same one. A security rule written down twice eventually disagrees with itself.
  • The upstream's own redirects are passed back to your visitor rather than followed, and a failure returns a plain error page that says nothing about what is behind it.

Deploy an image you built yourself

  • nesly deploy --image ghcr.io/acme/app:1.2.3 runs a container image you already built. Nothing is packaged, nothing is built, and the image is not rebuilt or copied into our registry — what you tested is what runs.
  • This is for teams with their own pipeline: your own base image, your own scanner, your own compliance step. Until now your only ways in were uploading a directory or connecting a repository, and both of them ended in our builder.
  • It does not count against your build minutes, because that quota measures build time we spend and here we spend none.
  • ⛔ A bare name like nginx or app:latest is rejected. On a laptop that quietly means Docker Hub; on a platform it means your domain could end up serving a stranger's image because of a typo. Name the registry. Digests and registries with ports both work.

Bring your own certificate

  • nesly certs add example.com --cert fullchain.pem --key privkey.pem installs a certificate you obtained yourself — an EV certificate, a wildcard, or one your organisation issues. nesly certs rm takes it off again and the automatic certificate takes over.
  • Before this, certificates were ours or nothing: nesly certs would show you what we had issued and there was no way to supply your own.
  • We check four things before storing anything: that the certificate and key are actually a pair, that the certificate covers the name, that it has not expired, and that the domain belongs to your project. A certificate that has expired is never served — the automatic one takes over instead of your site going dark.
  • ⛔ Your private key never enters the routing table. That table is fetched by every edge on a timer and was built to carry nothing usable; putting keys in it would send every customer's key everywhere, all the time. The key is asked for one host at a time, during the handshake for that host, and it is stored sealed.

Find the release that broke it

  • nesly bisect --good v12 --bad v31 -- ./check.sh searches the releases between two you already know about and tells you the first bad one. Nothing is deployed and nothing is rolled back: every release still has its own address, and the search only reads.
  • nesly rollback could undo a bad release but could not tell you which one it was, and there was no way to search a range by hand without a lot of tab switching.
  • Your command decides. We run it once per candidate with NESLY_URL set to that release's address; exit zero means good, anything else means bad. The check is written once and runs identically on every candidate — which is more reliable than answering a prompt twenty times, and it is why we did not add one.
  • A command that cannot run at all is an error, not a verdict. A typo in your script will not be reported to you as a broken release.

Allow or block a single address

  • nesly protect ip block 203.0.113.7 blocks one visitor. nesly protect ip allow 198.51.100.0/24 serves only your office. Both take single addresses or ranges, IPv4 and IPv6, and nesly protect ip off removes the rule.
  • Rate limiting slows a persistent source down and the country rule blocks a whole region, but neither could say anything about one address — not "this scraper, no" and not "the office, always".
  • Every uncertainty serves the visitor. No rule, an address we cannot see, an address or rule we cannot parse: all of them are served. A protection that fails closed takes your site down at exactly the moment something is already wrong.
  • An allow list with nothing in it is rejected rather than saved, because a half-typed command should not be able to close your site.
  • The order is address, then country, then rate — each one more absolute and cheaper than the next.

See who did what in your account

  • nesly activity lists what has been done in your account: who, what, which project, and when. Denied attempts are recorded too, which is usually the line you are looking for.
  • There was no record at all. That mattered more once you could put a user on a single project and once a variable could be copied from production into another environment — both are decisions worth being able to look back at.
  • The record is driven by the same registry that decides permissions, so an action that counts as a change is auditable the day it is written. A hand-kept list would go stale, and a missing audit line looks exactly like an event that never happened.
  • ⛔ We do not record visitor or IP addresses here. This is about your team and your account, not your traffic. Entries are kept for ninety days and then swept.
  • Recording never fails a request: if the record cannot be written, the work still happens and the failure is logged on our side.

Put a password on your preview addresses

  • nesly protect preview set asks for a password before anyone can open a preview address. nesly protect preview token issues a bypass token for CI, shown once. nesly protect preview off opens them again.
  • Preview addresses were open to anyone with the link, and the link is meant to travel — it goes in pull request comments, chat and email. A long address was never a check on who could read it, and what is on a preview is work you have not published yet.
  • ⛔ Preview addresses only. Your published site is never touched by this, whatever you set.
  • The password is stored as a hash and the bypass token as a digest, so what our edge holds cannot be used to get in. Changing the password signs everyone out who had the old one, without us keeping any session state.
  • Password attempts go through the same rate limiting as signing in.

Your preview no longer runs on the production database

  • Environment variables can now belong to an environment. nesly env set KEY=value --env preview sets it for previews only, --env production for the live site, and with no flag it applies to both, which is what all your existing variables do.
  • ⛔ This closes something that was wrong. A pull request preview used to be built and run with the credentials of your production database, on a public address. If you have a database from us, previews no longer receive its connection details at all — not at runtime and not during the build, so nothing can be baked into a public bundle either.
  • When a preview needs a database, give it one explicitly: nesly env set DATABASE_URL=... --env preview. A preview that has not been given one stops with a sentence telling you what to do, rather than a container that crashes in a loop.
  • nesly env share DATABASE_URL --with preview copies the production value across when that is genuinely what you want. It is a deliberate command with a deliberate name, and it is recorded in your activity log.
  • Existing variables were left exactly as they were, and no stored value was re-encrypted. nesly env ls --env preview shows you what a preview will actually see.

Choose which countries your site is served in

  • nesly protect countries --allow TR serves your site only where you say; --block RU,CN serves everyone except. It takes effect immediately, on every address the project answers on, previews included.
  • ⛔ Your visitors' addresses are still not stored, shown to you, or sent anywhere. The country is worked out at our edge from a local database and the address is forgotten. We considered showing you visitor IP addresses so you could block them individually and decided against it: once we start collecting that, the promise we made when the rate limit shipped stops being true.
  • A visitor whose country cannot be determined is always served. The data does not place every address — anonymising networks and new allocations genuinely have no country — and turning real visitors away over a gap in it would be worse than the traffic you are stopping. The same applies if our country data is unavailable: your site keeps serving.
  • This is the second half of the protection that started with rate limiting. A limit slows a scraper down; this blocks a source outright. Both are yours to set, neither is ever on by default, and the platform still never limits your visitors on its own.
  • Blocked visitors see a plain page saying the site is not available in their location — it does not name your rule, your country list, or suggest a way around it.

You can put a user on one project without giving them the account

  • nesly team invite client@example.com --project shop --role member gives that person the shop project and nothing else in your account. They do not see your other projects, their environment variables, their databases, their domains, your team or your billing.
  • This is what agencies and freelancers had no way to do. Adding a client to your account showed them every client you have; the only alternative was opening a separate account per client, and then you could not see your own work in one place.
  • The role means the same things it always did, inside that one project: an admin may manage who else is on it, a member may deploy and change it, a viewer may look without revealing secret values.
  • They join the way anyone joins — by signing in with the invited address, no code to paste. Their own account is still theirs; yours shows up in nesly account ls and the console's account switcher as the project or projects they were given, not as a role.
  • ⛔ We built the deny path before the feature. Everything in an account is closed to a user invited this way unless the request names a project they were actually given, so a screen or a command we add next year is closed on the day it is written rather than open until someone remembers to filter it.
  • A person given projects is a seat on your plan like anybody else, counted once however many projects they have. Removing them from a project takes effect immediately, including for access tokens they created.

One address can show one release while the rest of the site moves on

  • nesly domain pin staging.example.com <deployment-id> makes that address serve a specific release. Every other address keeps following your deployments, so the site goes on shipping while staging shows the build you are reviewing — or the version you are showing a client.
  • This is what nesly rollback could never do: a rollback moves every address you have at once. Pinning moves one and leaves the rest live.
  • A pinned release stays reachable for as long as the pin lasts. Normally an older release's address stops answering once a handful of newer deployments exist; a pinned one is exempt, because an address that dies on a schedule nobody set is worse than no feature at all.
  • It also cannot be removed while an address points at it — nesly rm rejects and names the address, so the fix is one command away.
  • nesly domain unpin puts the address back on whatever is live. nesly domain ls and the console both say when an address is pinned and to which release: an address serving an old version looks exactly like one serving the live site until someone notices the content is stale.

You can slow down one visitor without taking the site down for everybody

  • nesly protect set --per-minute 600 --burst 100 limits how quickly one visitor address may load your site. Until now, if you were being scraped or hammered from one place, the only lever you had was nesly maintenance on — which closes the site to every visitor you have. That is not a defence.
  • It is off by default and we never turn it on. The platform does not limit your visitors on its own: a site slowed down by us because it got popular would be our failure, not a protection. This is yours to set, on your own project, when the traffic hurting you is not popularity.
  • Two numbers, because a page is not a request: opening one page fetches its stylesheet, its script and its images at the same moment. --burst is how many requests one address may make at once; --per-minute is the speed it may keep up afterwards. A burst under 20 is not allowed — it would turn your own visitors away on their first page.
  • It takes effect immediately, without a deployment, and applies to every address the project answers on, previews included. Blocked visitors get a 429 with Retry-After, which browsers and crawlers read as a pause rather than an error.
  • It limits one address at a time, and it never sees who that address belongs to: we do not show you visitors' IP addresses and this feature does not need them.

Your CMS can rebuild your site, without holding the keys to your account

  • nesly hooks create "contentful, production" gives you a URL. POST to it and the project rebuilds from the branch it watches — which is how a content system publishes an article and the site updates itself. The console has the same on each project.
  • A hook can do exactly one thing: start a build of its own project. It cannot read your environment variables, download a backup, reach another project or change any setting.
  • ⛔ That distinction is the reason this exists. Until today the only credential we could give an outside system was an access token, and an access token carries everything its owner can do. Wiring up "rebuild on publish" therefore meant handing your CMS vendor the ability to read every secret and delete every project. If you have an access token sitting in a third-party system for this purpose, replace it with a hook and revoke it.
  • The URL is shown once and only its hash is stored, so we could not show it to you again even if we were asked to. Remove a hook and its URL stops working immediately.
  • One hook starts at most one build every ten seconds. A bulk import that fires hundreds of webhooks therefore costs you a few builds rather than a month of build minutes, and the calls that are turned away say so with a 429 so your CMS retries instead of assuming it worked.

Run a scheduled job now, instead of waiting for three in the morning

  • nesly cron run <name> runs a schedule immediately and prints what it printed. Until now the only way to find out whether a nightly job was written correctly was to wait for it to fire, so getting it wrong cost a day per attempt.
  • It runs the schedule itself — the same command, the same image, the same environment variables and the same time limit tonight's run would use — so the answer you get is about the job you actually have. The run appears in nesly cron runs and its output in nesly cron logs like any other, and the run list now says which runs were started by hand so a test of your own is never mistaken for the schedule firing.
  • When it runs next does not change, and a paused job can be run this way: paused means it does not start on its own, not that it cannot run. Fixing a broken schedule while it is paused is exactly when this is useful.
  • The platform never runs two copies of one schedule at once, and that still holds here: while a run is going, starting another is not allowed rather than queued.
  • --detach starts it and returns. Otherwise the command waits and exits with the run's own exit code, so it can be a step in a pipeline. The console has the same button on each scheduled job.

Deleting really deletes

  • nesly rm <deployment-id> removes one deployment: its record, its own preview address, and the files it published. The version currently serving your project cannot be removed, and neither can either side of a rollout in progress.
  • A removed deployment can no longer be rolled back to, and the command says so before it does anything.
  • ⛔ Correcting something we had wrong: nothing on the platform used to delete a published site. Superseding a deployment left its files in storage, and so did deleting the project or closing the account — even though this page said closing destroys everything in the account. It does now. If you have closed a project or an account before today, its files were still held; they are removed by the same path from now on, and you can ask us to remove any that predate this change.
  • Nothing is deleted on a timer. A deployment you can still roll back to is a deployment whose files are still there — a rollback target that had quietly lost its files would fail at the only moment anybody reaches for one.

Set your own response headers

  • [[header]] in nesly.toml sets response headers on the paths you name. Caching, X-Frame-Options, a content security policy, CORS — a static site is served straight from storage, so until now there was nowhere at all to say any of it.
  • for matches the way a redirect's from does. Unlike redirects, every rule that matches is applied: broad security headers on /* and a year-long cache on /assets/* at the same time. Where two rules name the same header the one further down wins.
  • A rule replaces what your application sent for that header rather than adding a second value beside it, so a browser is never left choosing between two contradictory instructions.
  • The rules ship with the deployment, so a rollback restores the headers that version was serving with — a cache rule never outlives the assets it was written for.
  • Names that describe how a response is framed are rejected, and so are Strict-Transport-Security and Set-Cookie: the first is the platform's HTTPS guarantee, the second is how a gradual rollout keeps a visitor on one side. A line break in a value is rejected as well. All of these fail the deploy rather than being quietly ignored.

Changing one notification setting no longer changes the others

  • PUT /v1/notifications/settings now leaves out what you leave out. Sending only {"language":"tr"} used to switch your deployment, usage and backup mail off — the API replaces the whole object, and those three fields could not tell "off" apart from "not mentioned". Nothing said so, and you would have found out at the next failed deployment nobody told you about.
  • If you script against this endpoint, you can stop sending fields you do not want to change. Requests that send every field keep working exactly as before.
  • The console and nesly account notifications set were never affected: both read your settings and send them all back. Nothing you changed there was ever lost.

Old addresses keep working

  • Redirect rules in nesly.toml: [[redirect]] with from, to and an optional status. A site moving onto the platform can keep every address it arrived with, static sites included — until now there was no way to answer a moved URL at all, because a static site is served straight from storage with nothing of yours in the path.
  • from = "/blog/*" covers a whole section, and :splat in the destination carries the rest of the path across. /blog itself is covered too, so the section index is not the one address left behind.
  • The query string comes across, so a moved link with campaign parameters still reports the way you expect.
  • Rules belong to the deployment, not the project: a rollback brings back the pages and the redirects that shipped with them, together. nesly inspect prints the rules a deployment actually went live with.
  • A rule that cannot work stops the deploy instead of reaching visitors — a destination that is neither a path nor a full address, a status that is not a redirect, two rules for one address, or a rule pointing at itself.

Every notification has a switch, on every surface

  • Certificate warnings can now be turned off. They could not be, anywhere — the message was sent and there was no switch for it on any screen. There is one now, in the console and in nesly account notifications set --certificates=false.
  • Application errors and scheduled-job failures now have switches on the command line too, next to deployments, allowances and backups. Before this they could only be changed from the console.
  • nesly account notifications show prints what reaches you without changing anything. Until now the only way to see your choices from a terminal was to run set, which wrote them back.
  • Setting one switch leaves the others alone, and so does setting the language.
  • Billing messages still have no switch: a declined card and a suspension always reach you.

Send your logs to your own log system

  • nesly logs drains add <url> forwards everything your application writes to an address of your own — your log platform, or a service you wrote. It keeps going with nobody watching, which is the difference between this and nesly logs.
  • Every instance of every deployment, and every background process, in one stream. Each line says which deployment, which worker and which instance wrote it, so "one replica is misbehaving" is a question you can answer.
  • Most collectors want their own key in a header, so you can attach up to five with --header 'DD-API-KEY: your-key'. The values are stored encrypted and never shown again.
  • Each batch is signed, with the same scheme as webhooks — the signature covers the body and the moment it was sent, so a captured batch cannot be replayed at you later. If you already verify our webhooks, that code works here unchanged.
  • Nothing moves forward until your receiver accepts it. A batch that fails is sent again from the same place, so a restart on your side does not leave a hole in your logs.
  • If a drain stays broken long enough that lines pass out of the seven-day window, you are told twice: in nesly logs drains list, and as a line at the top of the next batch that does arrive. A drain that quietly skipped a day would be worse than no drain.
  • nesly logs drains test sends one line now and tells you what your receiver answered, in a sentence — so the first delivery you ever see is not the one during an incident.

Your application's logs stop disappearing

  • nesly logs now answers after the fact. Until now your application's output was read off the running container when you asked for it — so if the application crashed and scaled away, the explanation went with it, and the moment you most needed the logs was the moment there were none.
  • The output is collected as it is written and kept for seven days. That covers the case this was built for: you find out on Monday that something broke on Saturday night, and the answer is still there.
  • You see every instance, not one. The old read picked a single container, so with more than one running you were looking at a fraction of your traffic's output and had no way to know it.
  • Background processes are covered too — nesly logs --worker <name> reads the same stored history. A worker that scaled away used to be just as silent as a server that did.
  • Nothing is dropped quietly. There is a per-project ceiling on how much is collected each hour, and if you reach it, a line in your own log says so. A limit you are not told about is indistinguishable from a bug.
  • Very long lines are cut at 8,000 characters and marked where they were cut, rather than silently shortened.

Transfer a project to the customer who will own it

  • nesly project transfer <project> --to <account> moves a project to another account: the site, its deployments, its domains, its databases and its backups. Until now an agency finishing a site had no way to give it to the customer except to delete it and build it again in their account — throwing away the address, the certificates and the history in the process.
  • The site does not go down and nothing is rebuilt. Everything the platform runs for a project is keyed by the project itself, not by the account, so a transfer is a change of owner and not a migration.
  • The repository connection is removed by the transfer, and you are told so. It deploys with the previous owner's GitHub access, so carrying it over would leave them able to deploy a project that is no longer theirs; the new owner connects the repository from their own account.
  • You have to be a member of the receiving account, with the right to create projects there. Nobody can push a project — and its bill — at a user who did not ask for it. Usage already recorded stays with the account that served it.
  • nesly project rename changes what a project is called, and nothing else. Its address stays exactly as it is, so no link, DNS record or integration breaks — and the command says so every time rather than leaving you to find out.
  • nesly project info answers "what is this project, right now" on one screen: its address, who owns it, what is live, the domains pointing at it, the databases attached to it, and whether visitors can see it.

A migration no longer means taking the risk

  • nesly maintenance on puts a page in front of every address your project serves while you do something that would break it — a schema migration, most of the time. Until now the honest description of that procedure was "take the risk": the site kept serving while the schema moved under it, and your visitors got whatever the old code did against a half-migrated database.
  • It covers preview addresses too, not only your domains. A preview runs with the project's own variables and therefore talks to the same database, so leaving previews up would leave a second door open onto the migration you are in the middle of.
  • nesly run, nesly proxy and nesly db keep working while it is on. They never went through the edge, which is exactly why the page can go in front of visitors without getting in your way.
  • --message puts your own sentence on the page. We do not guess your visitors' language: without it they get an English default, and with it they get whatever you wrote.
  • Visitors get a 503 with a Retry-After, which is what tells a monitor the site is down on purpose and stops search engines from dropping your pages. An unpaid account still shows its own notice — billing is not something a switch in your project can talk over.
  • nesly deploy and nesly rollout promote tell you when the project is still in maintenance, because "deployed" and "live" are not the same sentence while a page is up.

Release to a tenth of your visitors first

  • nesly deploy --percent 10 puts the new release in front of a tenth of your visitors and leaves the rest on the release that is live. Until now a deploy was all or nothing: it worked for everybody or it was broken for everybody, and you found out from the people who were already on it.
  • nesly rollout shows both releases side by side — the share each is taking, how many requests each served, and how many of those failed. It is deliberately the same line the traffic tables draw: a 404 is the visitor's mistake, a 500 is yours.
  • nesly rollout promote gives the new release everything; nesly rollout abort sends everybody back, immediately. Aborting keeps the abandoned release, its own address and its logs — the moment you decide a release is bad is the worst possible moment to delete those diagnostics.
  • A visitor who lands on one release stays on it. That is not a nicety: a page is not one request, and a visitor who got the new HTML with the old script bundle would be looking at a broken site while the percentages looked perfect.
  • Your background processes and scheduled jobs stay with the release that is live, and move when you promote. A release holding a tenth of the traffic must not quietly take the whole queue.
  • The console marks both releases with the share each is serving, so a rollout started from a terminal is not invisible to everybody else.

Your team, from the terminal

  • nesly team ls, invite, remove and leave bring team management to the command line. Invitations still work the same way: the person joins the moment they sign in with the address you invited, and there is no code to paste.
  • nesly account use <name> switches which account the other commands act on, so a user who was invited to a company account can finally deploy it from a terminal instead of only from the console. nesly account ls shows the accounts you can work in.
  • In CI, pass --account or set NESLY_ACCOUNT — those apply to a single command and are never written to disk.
  • If the account you selected is one you can no longer use, commands stop and say so instead of quietly doing the work in your own account. That mattered more than it sounds: a linked directory remembers a project by name, so the same name in your own account would have accepted the deploy.

Your domain goes live by itself

  • Create the DNS records at your provider and stop there. The platform re-checks a domain that is not yet serving every couple of minutes and activates it on its own — you no longer have to remember to come back and run verify.
  • That was the whole gap: we told you "give your DNS provider a minute and try again", and then, when the minute passed, nothing happened until you typed the command yourself.
  • A domain that was rejected is re-checked too. If you fixed a typo in a record, you do not need to know that a command exists — the next round picks it up.
  • The reason shown against a domain is now the current one rather than whatever it was the last time someone checked manually. A stale reason is worse than none: it sends you to fix something you already fixed.
  • nesly domain verify --wait blocks until the domain is live, for when you would rather watch it happen than come back later.

You can see your certificates, and we tell you before one expires

  • nesly certs shows the HTTPS certificate every one of your addresses actually serves: who issued it, when it expires, and whether anything is wrong with it.
  • It is measured, not looked up. The platform opens an HTTPS connection and reads the certificate that comes back — the same one your visitors get. A renewal that failed quietly leaves our records saying "issued" while every browser shows a warning, and only the connection tells those apart.
  • If a certificate is within two weeks of expiring and has not renewed, you get an e-mail. Renewal normally happens a month ahead, so at that point it is not late — it is not working, and there is still time to fix it.
  • The same message covers a certificate that is served for the wrong name, which a browser rejects just as hard as an expired one.
  • It is its own switch in your notification settings, and it is on to begin with. This is the only failure on the list that nobody can see coming by looking: a certificate works perfectly until the hour it does not.

Try a change before anyone else sees it

  • nesly deploy --preview builds and deploys exactly as a normal deploy does, then serves the result at an address of its own. Your live site is left exactly as it is.
  • Every preview keeps its own address, so you can send one to a colleague, open two side by side, or come back to last week's.
  • A preview never becomes the live deployment, however green it is, and it does not start your project's background processes — a preview cannot disturb the queue your production is working through.
  • Previews already existed for pull requests. This is the same thing, asked for from your own machine, with no repository and no pull request needed.
  • nesly deploy still deploys to production, and still does so when it is run with no flags at all. Nothing that works today changes.
  • --prod says that out loud. It changes nothing on its own, which is the point: a pipeline that states what it means keeps stating it.

Your database, in the tool you already use

  • nesly proxy opens your project's database on a port on your own machine. Point TablePlus, DBeaver, DataGrip, a migration runner or a one-off script at it and you are browsing your own data.
  • The command prints a connection string you can paste straight into your tool: your own user, your own database, pointed at the tunnel instead of the cluster.
  • nesly db opens a prompt using the client already installed on your machine — psql for PostgreSQL, sqlcmd for SQL Server. Your history, your settings, your copy and paste.
  • The database stays off the public internet. The tunnel is authenticated as you, limited to one project, listens only on this machine, and closes when you press Ctrl-C.
  • Your password is passed to the client through the environment, never as a command-line argument, so it does not show up in the process list or in your shell history.

A shell inside your application

  • nesly run --tty -- bash gives you a prompt in your own image, with your own environment variables and your own database reachable. So does rails console, php artisan tinker, psql, or anything else that expects a terminal.
  • It runs in a new container, not the one serving your visitors. Same image, same variables, its own process — you can explore, break things and exit without anyone's request noticing.
  • The window size travels, so your prompt wraps where you expect it to, and it keeps travelling when you resize the window mid-session.
  • The container is created for the session and removed when you leave it, however you leave it.
  • Without --tty nothing changes: the command still runs, prints and exits with its own code, which is what a CI pipeline wants.

Your project's variables can come to your machine

  • env pull writes a .env you can run locally — the same variables the deployed container sees, including the database ones we derive from DATABASE_URL. Until now the only way to work locally was to read them off a screen and retype them.
  • It holds real secrets, so it says so: the file is written readable only by you, and if git is not already ignoring it you get the line to add, before you commit rather than after.
  • env get KEY prints one value and nothing else, so $(nesly env get DATABASE_URL) works in a script.
  • env run -- <command> runs something on your machine with the project's variables loaded and writes nothing to disk at all — the one to use in CI.
  • An existing file is never replaced without --force. If you hand-edited a local override, it stays yours.

Run more copies of a background process, or restart one

  • Until now the process list was something you could read and not something you could change. If a queue consumer wedged, the only way to move it was to deploy again.
  • ps scale worker=3 runs three copies. ps scale worker=0 stops it without forgetting it — it stays in the list and starts again the moment you scale it back up.
  • ps restart worker replaces its copies one at a time, so a process running several of them never drops to none.
  • The count survives your next deploy. It is part of your project, not something set on the running copy and lost on the next push.
  • The web process is not scaled this way: it already scales on the traffic it receives, and the number of copies it never drops below is part of your plan.

We tell you when a scheduled job of yours fails

  • A scheduled job runs while you are doing something else — that is the point of it. Until now, if one failed, the only way to find out was to open the console and look.
  • You now get an e-mail within a minute, naming the job and what became of the run: the code your command exited with, or that it reached its time limit and was stopped.
  • A run that could never start ends the same way as one that took too long, so the message says so instead of guessing. It also says where the output is, and when there is likely to be none left.
  • It is its own switch in your notification settings, separate from deployments, and it is on to begin with.

We hear about our own failed jobs in minutes

  • Some of the platform's own work runs on a schedule. The nightly backup of the database that records your projects, domains, deployments and usage is one of those runs.
  • If one of them failed, or started and never finished, we could take up to a day and a half to find out. We now know within ten minutes, and so does whoever is on call.
  • The gap was a precise one, and worth naming: a run that cannot start at all — because an image it needs has gone missing, say — keeps retrying rather than failing, so asking "did it fail?" got the answer no. We now ask whether the run finished, which covers both.

Everything you read is set in one typeface

  • Identifiers used to be printed in a second, narrower font: your tenant id, an API token, a domain, a commit, the tax number in the footer. Put next to ordinary sentences in a table or a list, that reads worse rather than clearer — two fonts on one screen make both harder to scan.
  • They now use the same typeface as the rest of the page, and are set apart by weight instead. Numbers that sit in a column — exit codes, request counts, timestamps — keep their columns lined up with tabular figures.
  • Terminal output has not changed. Build and runtime logs, the shell examples in the docs, and a build's own error text stay in a fixed-width font, because whatever produced them lined them up in columns and a proportional font would break that.
  • Two alert emails were sending a stray pair of accent marks around a command, left over from how the sentence was written. And the worst route in an error-rate alert is no longer boxed as if it were program output — it is one line, and it now reads like one.

Your application sees the hostname your visitors typed

  • Until now a request arrived at your application carrying our internal name for it — something like dep-4k2j.prj-9x1m.nesly.net — instead of the address the visitor actually asked for. The name changed on every deploy, so there was nothing stable to write down.
  • This breaks the host check that good frameworks do by default: Laravel's trusted hosts, Django's ALLOWED_HOSTS, the equivalent in Rails and Next.js. And our own advice, when it did, was to switch that check off. We were asking you to disable a security control because of a defect on our side.
  • Your application now receives the hostname the visitor used — your own domain, your project address, or the preview address for that deployment. You can list your real domains and leave the check on.
  • One honest limit: this settles when a project deploys. A project that has not deployed since today still behaves the old way until its next deploy, and nothing breaks in the meantime.

Adding a domain tells you what is actually wrong

  • A correctly configured domain could be rejected, and the rejection was convincing. If you checked in the console before the record had spread — which is the natural thing to do — the answer we got back was remembered for five minutes, and every attempt inside that window failed on the memory rather than on your DNS. Your own early check was what made the domain unverifiable.
  • Verification now asks the nameservers your domain is delegated to, so it reads the record you just wrote instead of a stale copy of its absence.
  • A root domain pointed at us with A records verifies as well as one on a CNAME. Providers that flatten a CNAME at the apex, Cloudflare among them, no longer look like a misconfiguration.
  • When it does fail you get a sentence about your domain: the record is not there yet, or it points somewhere else, and here is what it points at. It used to print one of our internal addresses, which told you nothing about your own setup.

The CLI works where you are standing, and says why

v0.11.0
  • nesly logs, nesly ps, nesly traffic and nesly domain demanded --project even inside a directory you had already linked. nesly deploy in the same directory needed nothing. logs is the first thing you reach for when something is wrong, and it was the one asking you to look up an identifier.
  • They now use the link, like every other command. --project still works and still wins, for scripts and for the times you are somewhere else.
  • When the platform rejects a request, the CLI prints the reason it gave. Before, the reason was thrown away and you got a bare failure — which is why a domain that was one record away from working looked the same as one that was hopeless.
  • Suggested commands can now be pasted and run. Nine hints were printing --project with nothing after it; copying one gave you a command that could not work, in the moment you were already stuck.

A worker that crashes is reported as crashing

  • A background worker that failed on startup — a bad connection string, a missing variable, an exception before the first job — was reported as running. The deploy finished, the log said the process was up, and you found out from the work not getting done.
  • The deploy now waits for a worker to stay up rather than merely start, and tells you which state it reached: running, or crashing and restarting, and how many times.
  • It also waits for the version you just deployed. It used to be possible to read the health of the process you were replacing and call it good news.
  • Separately, in the console: the language switcher was labelled in English for screen readers on the Turkish interface. It reads correctly now, and there is a test that fails if any label in the interface loses its translation again.

You can check a webhook before you need it

  • Registering a webhook used to be an act of faith. You pasted a URL, wrote a verifier against our documentation, switched it on — and then nothing happened, by design, until something went wrong. The first delivery you ever saw arrived during an outage.
  • There is now a Send test button beside each endpoint, and nesly webhooks test. Both send one real signed delivery and tell you what your server answered.
  • When it fails you get a sentence, not just a transport error: nothing answered within ten seconds, that host name does not resolve, your server rejected it — check that it is verifying the signature with the current secret. The technical detail is still there underneath, for whoever runs the server.
  • The ping carries kind "test", which your receiver should ignore. One attempt, no retries: you want the truth about this request rather than a summary of three.
  • It works on an endpoint that is switched off, and whatever your notification settings say. Checking that something works before you turn it on is the right order, and a test is a request you just made, not news we decided to send you.
  • Tests show up in the delivery log like any other delivery, and a successful one clears a run of failures.

Notifications can now reach your own systems

  • Everything the platform tells you used to end at an e-mail address. An inbox is where an alert goes to be read tomorrow; the team that would act on a failing checkout has a channel and a rotation, and neither of them reads the account owner's mail.
  • You can now register up to five URLs of your own. Every notification we send — a failed deployment, an allowance running low, a backup that did not complete, a project returning errors — is also posted there as JSON, so your own tooling can pick it up.
  • Each request is signed. The signature covers the body and the moment it was sent, so a captured request cannot be replayed later against you. Verifying it is about ten lines; the worked example is at nesly.net/docs/webhooks.
  • The switches you already have govern both. A category you turn off does not reach your webhook either — turning something off should mean off, not off in one place.
  • We keep a record of every attempt for seven days, with the status your server answered, because "did it fire?" is a question asked during an incident and it deserves an answer rather than a guess.
  • Your address has to be on the public internet, and we check that against the address we are actually about to connect to rather than the one you typed. A name that later points somewhere private is rejected at that moment, not trusted because it looked fine when you added it.
  • The signing secret is shown once. If you lose it, rotate it — we would rather not be able to show it again than have it sitting in a response a user logs.

We tell you when your application starts failing

  • Everything we have shipped about traffic so far answers a question you have to think to ask. The console shows you what is failing — if you open the console. Now the platform tells you first.
  • When your project starts returning errors to a meaningful share of its visitors, you get an e-mail. It names the route, the exact status your visitors received, and what that status means: a 500 is your code throwing, a 502 is nothing listening, a 503 is your application not ready to take the request.
  • You get one message when it starts and one when it is over. Not one every five minutes — an alert that fills your inbox is an alert you stop reading.
  • We only tell you when the numbers mean something: at least 20 requests in fifteen minutes, and more than one in ten of them failing. One failure out of three is not a measurement, and we are not going to wake you up for it.
  • Recovery has to be earned. If your project simply stops serving traffic we do not send an all-clear — silence is not the same as fixed.
  • Requests for pages that do not exist never count. A site being scanned should not read as a broken one.
  • New switch in your account settings, on by default. An outage alert that waits until you find the switch protects only the people who already knew where it was.

Not just how many requests failed — how they failed

  • Until now a failed request was one word: 5xx. That word covers a 500, where your code ran and threw; a 502, where nothing was listening at all; and a 503, where your application was not ready to take the request. Those are fixed in different places, and you could not tell which one you had.
  • The Traffic tab now shows the failures themselves. Open a failing route and you get the individual requests: the exact status each visitor received, the second it happened, and how long the request took before it gave up. Each one says in a sentence what that status means.
  • That last number matters more than it sounds. A 500 in three milliseconds and a request that timed out after thirty seconds are opposite problems, and averaged together with everything that worked, neither is visible.
  • In the terminal: nesly traffic failures. Narrow it with --route /api/checkout or --status 502, widen the window with --since, and --json gives you one object per failure for scripts and agents.
  • When an application is failing thousands of times an hour, not every request is recorded separately — that would be a copy of your traffic. The list always says how many actually failed, so it never reads as though the problem were smaller than it is.
  • Individual failures are kept for 7 days. The route totals stay for 30, so you can still see how often something failed last month.
  • Only failures inside your application are listed. A visitor asking for a page that does not exist is not an outage, and a site being scanned should not read as a broken one.
  • The same privacy rules as the rest of traffic: the route is the normalized one — /users/:id, never /users/42 — and no query string, IP address, user agent or header is recorded anywhere.

Traffic in the console — which route broke, and when

  • Your project page has a Traffic tab. It shows what your application actually served over the last hour, day, week or month: how many requests, how many of them your code failed to answer, and how slow the slowest 5% were.
  • The chart shows failures as a band on top of the requests they happened in, so an outage is a shape rather than a number you have to compare with another number. Underneath it, a second line tracks response time over the same window — a route that got slow at 14:00 looks like it got slow at 14:00.
  • Selecting a route narrows the chart to that route alone. That is how you find out whether the problem started with your last deploy or has been there all week.
  • The route table carries requests, failures, failure rate and p50/p95/p99 for every route and method, sorted by traffic, by failures or by slowness.
  • Every failing route is called out under the chart with a link to your logs, because a table that shows you a problem and stops there has only told you that you have one.
  • Failures mean your application returned a 5xx. Requests for pages that do not exist are counted separately, on the route that received them — a site being scanned should not read as a broken one.
  • The console and nesly traffic read the same data and agree with each other, and both reconcile with what you are billed for the same window.

nesly traffic — which route is failing, and how slow it is

v0.10.0
  • Your project's requests are now grouped by route and method, with how many failed inside your application and how slow the slowest 5% were. Until now the only numbers you could see were a total request count and a total egress figure, which tell you that something happened but never what.
  • Paths that only differ by an identifier are one row: /users/42 and /users/43 both appear as /users/:id, so a busy site stays readable.
  • Server errors and client errors are counted separately. A page that gets scanned for /wp-admin should not read as a broken application, so the failure rate counts only what your code failed to answer.
  • nesly traffic --sort errors ranks routes by how many requests actually failed, and --sort slowest by the slowest 5%. --since widens the window, --json is there for scripts and agents.
  • The numbers reconcile with your bill: for any window, the route rows add up to exactly the requests you are charged for.
  • Query strings are never recorded — not the values, not the keys. Neither are IP addresses, user agents or headers.
  • Traffic history is kept for 30 days.

nesly ps — see whether your processes are actually running

v0.9.0
  • Logs cannot tell you that a background process has died. A worker that crashed on its first line and a healthy one with nothing to say both print nothing, so the one question you have — is it running? — had no answer anywhere. nesly ps answers it.
  • It lists every process: your web process and each worker from your Procfile, with its state and how many times the platform has restarted it. A process that keeps failing reads as crash-looping, with the exit code, when it last exited, and the exact command to see what it printed.
  • The states are running, starting, crash-looping and stopped. Starting is not treated as a problem — a process is allowed a few seconds after a deploy without anyone raising an alarm.
  • nesly --json ps gives the same thing as an object per process, for scripts and agents.

Two releases you never got to read about

v0.8.1
  • This changelog was missing the entries for two releases. They are above now, dated to when they shipped: nesly login used to fail from a signed-out state, which is the one state you would be running it in (v0.6.0), and nesly deploy used to give up with "check your connection" while your build carried on running perfectly well without you (v0.6.1). Both were fixed at the time and neither was written down.
  • Every release since the first now points at the entry describing it, so the version you have installed can be traced to what changed in it.
  • Smaller: one row of nesly limits and of the console's account page was showing an internal name instead of a description. It reads as a sentence now, like the rest.

Your application can run more than one process

v0.8.0
  • A worker: line in your Procfile now runs. Until today nesly read the file only to check it was there, so worker: celery -A app worker was accepted, never started, and never mentioned — the web process shipped and the queue simply went unconsumed. Whatever you declare beside web: is a real process now.
  • A worker runs your deployment's image with your project's environment, including an attached database, and it stays running: there is no traffic to scale it on, so it is not scaled to zero between jobs. Nothing routes to it and it needs no port.
  • nesly logs --worker <name> streams one process's output on its own, and the deploy tells you whether each one started before it says ready.
  • You can declare the same list in nesly.toml with [[worker]] blocks if you would rather keep it beside your build settings; the file wins over the Procfile.
  • A line that cannot be read is now rejected with its line number instead of being dropped. That is the point of the whole change: a process that quietly does not run is the one problem you cannot notice.
  • Background workers are part of the pro plan. On the free plan a deploy is not rejected for having them — your site ships exactly as before, the workers do not start, and the build log names which ones and why.
  • A worker uses memory around the clock, so it is counted in your usage the same way your application is. Worth a look at the usage page if you add one.
  • Preview deployments do not run workers, so opening a pull request can never take over the processes your project is running.

Scheduled and one-off runs are counted in your usage

v0.7.0
  • A scheduled job and a nesly run command use memory exactly the way your application does, and until now neither appeared in your usage. They do now: each run is counted for as long as it actually ran, and it shows up in nesly usage and on the console's usage page under the same memory figure as everything else.
  • Nothing is charged retroactively — runs from before this change produced no measurement and none has been invented.
  • If you run something every minute, this is worth a look at your usage page before it reaches an invoice.

Your project can run something on a schedule

  • nesly cron add scheduler --schedule "* * * * *" -- php artisan schedule:run runs a command inside your deployed application on a clock you set. It sees exactly what the application sees: the same image, the same environment variables, the same database. Laravel's scheduler works without anything else to configure.
  • The schedule belongs to the project, not to a release. Deploy again and the next run uses the new code; roll back and it uses the code you rolled back to.
  • Times are UTC unless you name a time zone, so --timezone Europe/Istanbul with 0 3 * * * means three in the morning where you are. The CLI and the console both show you when the job fires next, before you save it.
  • nesly cron runs shows the recent runs with their exit codes and nesly cron logs shows what the last one printed — the two things you want at nine in the morning after a job failed at three. Pausing keeps a job's schedule without running it.
  • The console has a Scheduled jobs tab on each project with the same list, the same next-run times and the same output.

A migration reaches the same database your application does

  • nesly run now gets the full environment your application gets, the attached database included. nesly run -- php artisan migrate used to fall back to Laravel's sqlite default and fail against a file that is not in your image, while the deployed app was talking to PostgreSQL perfectly well.
  • nesly diagnose knows about the variables the platform supplies, so it no longer reports one of them as missing — advice that, if followed, switched the whole managed connection off.
  • The rule behind that is now stated where it applies: a database's variables are one decision, so defining any one of them yourself means your project supplies all of them. nesly env set, nesly env ls and the project's Environment variables card name exactly which ones the platform is no longer adding.

A Laravel boot stops warning about variables that are not missing

  • Starting a Laravel application printed a warning for every optional variable its own configuration mentions — AWS_BUCKET, POSTMARK_TOKEN, SLACK_BOT_USER_OAUTH_TOKEN and dozens more — none of which is a problem, all of them above the error you actually needed to read. They are gone.
  • The warning that matters stays: an application key that is not set still says so, because Laravel genuinely cannot serve a request without one.
  • For the variables your app really does need, nesly diagnose still names them, with the supporting log lines and the command that sets each one.

Your database arrives under your framework's own name

  • A managed database still appears as DATABASE_URL, and the same credentials now also reach your container as Laravel's DB_URL and DB_CONNECTION / DB_HOST / DB_PORT / DB_DATABASE / DB_USERNAME / DB_PASSWORD, and as .NET's ConnectionStrings__Default. A Laravel or ASP.NET project connects to it with no configuration code at all.
  • What you set yourself always wins: if your project already defines any of the DB_ variables, none of them are filled in for you.
  • Nothing extra is stored on your project, so these can never collide with a variable of your own, and a project that attached its database months ago gets them on its next deploy.
  • You can see them: nesly env ls lists them under their own heading, and the project's Environment variables card shows them as a separate group. They are named, not valued — the connection itself is the DATABASE_URL you already have.

The console opens on your projects

  • Signing in now lands on your projects — every one of them as a card with its branch, its last commit and whether that deployment is live — instead of four usage counters.
  • Consumption moved to its own Usage page, which says in one line that its counters include projects you have since deleted.

A deploy no longer gives up while the build is still running

v0.6.1
  • nesly deploy streams your build as it happens, and if that stream broke — a network blip, a restart on our side — the command stopped and said "check your connection". The build itself kept going and usually finished fine, but you had no way to know that from the terminal.
  • It now reattaches and carries on from where it stopped, without reprinting the lines you already read. It only gives up if the platform stays unreachable for three minutes, which is a real problem rather than a moment of silence.

Signing in works when your session has expired

v0.6.0
  • nesly login sent your stored credential along with the request, so once a session had been revoked or had expired, the one command meant to fix that failed too — with "token refresh failed, run nesly login again", printed by nesly login itself.
  • Signing in now asks the platform who to authenticate you with before presenting anything, so it works from a signed-out state. Every other command still sends your credential as before.

Hugo and MkDocs starters

v0.5.0
  • nesly new hugo-site and nesly new mkdocs-docs scaffold a working static site or docs site — the Hugo starter ships its own layouts, so there is no theme submodule to lose on deploy.

Docs in markdown, for agents

  • Every docs page now has a markdown twin: replace the trailing slash with .md (docs/builds/ → docs/builds.md). Generated from the rendered page at build time, so it can never drift from what humans read.

A Claude Code plugin

  • Install nesly as a Claude Code plugin — /plugin marketplace add neslyhq/claude-plugin, then /plugin install nesly@nesly — and Claude gets the MCP tools plus a skill that knows the deploy workflow: the $PORT contract, build detection, and diagnose-first when something fails.
  • The manual path still works everywhere else: any MCP client can run nesly mcp on stdio.

Hugo, Jekyll and MkDocs deploy like everything else

  • The three big non-Node static site generators are now detected and built with their own toolchains — hugo --minify, bundle exec jekyll build (or stock Jekyll without a Gemfile), mkdocs build — and served as static files.
  • Deploy guides with the real pitfalls (submodule themes, macOS Gemfile.lock, requirements.txt for your MkDocs theme): /deploy/hugo/, /deploy/jekyll/, /deploy/mkdocs/.

SQL Server restore, into the platform

  • nesly backup restore now works for mssql databases — the same contract as postgres: a safety copy of the current data is taken first, then the archive replaces the database, and the tenancy fence is re-established.
  • Downloading the .bak and restoring it elsewhere still works; nothing was taken away to add this.

A doomed deploy is rejected in seconds, not minutes

  • A Python project with no start command used to build, start, exit, and time out five minutes later. It is now rejected before the build, with the exact nesly.toml lines that fix it.
  • nesly build show prints the same warning before you deploy anything.
  • nesly diagnose now catches a nesly.toml key the platform ignored (a build setting outside [build], or a typo) and quotes the build-log warning in the finding.

Deploy guides and honest comparisons

  • Nine framework guides (/deploy/nextjs/ … /deploy/go/) that say exactly how detection sees your project — sourced from the detection code, not marketing.
  • Five comparison pages (/vs/vercel/ … /vs/heroku/) that list what the other platform has that we do not, in writing, before you migrate.

Templates: two commands to a live URL

  • nesly new lists deployable starters — Astro static, Node + PostgreSQL, Python FastAPI, .NET + SQL Server — and nesly new <template> copies one into a fresh directory with the commands to go live.
  • Every template is a complete, readable project in the public neslyhq/templates repository, deployed live before it is listed.
  • The /templates/ gallery shows the same list the CLI reads — one manifest, no drift.

SQL Server for .NET projects

  • nesly addon add mssql attaches a managed SQL Server database (Express edition, 10GB per database) with the same tenancy fence as postgres: your own login, your own database, other projects' names not even visible.
  • Next to DATABASE_URL it injects ConnectionStrings__Default — an ASP.NET app reads it with GetConnectionString("Default"), zero configuration code.
  • Nightly backups produce a standard .bak you can download and RESTORE DATABASE anywhere. Restoring back into the platform is not there yet, and the CLI says so instead of pretending.

Built to be operated by AI agents

  • nesly mcp serves the whole platform as MCP tools from the CLI binary you already have — deploy, logs, env, domains, addons, rollback. claude mcp add nesly -- nesly mcp.
  • nesly diagnose (and the diagnose_deployment tool) explains a failed deployment: the log lines that support each finding, env vars your app reads but the project does not define, and the command that fixes each finding.
  • A failed nesly deploy now points at nesly diagnose instead of leaving you with a raw log.
  • AI assistants fetching nesly.net receive the agent briefing (llms.txt) directly; the new /docs/agents/ page documents the MCP setup and the --json contract.

Trust pages: security, status and this changelog

  • A /security page that lists only what is actually built — isolation, image scanning, encryption at rest, network fencing — and says openly what is not there yet.
  • A public status page tracks the site, console and API from outside our infrastructure.
  • Vulnerability reports now have a standard address: /.well-known/security.txt (RFC 9116).
  • Contact page: pre-addressed mail links for support, billing and security, and a line for enterprise / bank-transfer questions.

Plan allowances, notifications, and your data being yours

  • Plans now state exactly what they allow (projects, build minutes, traffic, team seats, backups) — and the platform enforces the same numbers it shows. nesly limits prints yours.
  • When an allowance runs out, your site keeps serving; only new builds wait for the next month.
  • E-mail notifications: failed production deploys, 80% and 100% usage warnings, backup failures, billing events. Per-category switches in the console; billing mail is always on.
  • Export everything the platform holds about your account as one archive (nesly account export), and close the account yourself — guarded by typing the account's e-mail address.

Backups, one-off commands, and root domains

  • Managed databases are backed up nightly; list and restore from the console or nesly addon backup.
  • nesly run executes a one-off command (migrations, seeds) with the same image and environment as the running app.
  • Custom domains now work at the apex (example.com, not just www) with per-domain certificates.
  • Deleting a database no longer drops it instantly — it is fenced off and kept as a tombstone first.

GitHub App: push to deploy, PR previews

  • Connect a repository through the nesly GitHub App — private repositories included, no deploy keys to manage.
  • Every push to the production branch builds and deploys; every pull request gets its own preview URL and a commit status.

Build settings, machine credentials, and a platform that explains itself

v0.4.0
  • Framework detection with overrides: build and start commands, root directory, nesly.toml committed next to your code.
  • Access tokens for CI (nesly token): deploy from a pipeline without a browser session; revocation is immediate.
  • Team roles are enforced on the server — owner, admin, member, and a read-only viewer.
  • Environment variable values are encrypted at rest; keys live in a secrets manager.
  • Visitors of a paused or broken app see a real status page instead of a blank error, with correct Retry-After.
  • Sign-in endpoints are rate-limited. Build logs survive platform restarts.
  • The whole control plane installs from one Helm chart.

The public edge

v0.3.0
  • nesly.net, console.nesly.net and api.nesly.net are live behind the platform's own edge with TLS termination.
  • The CLI talks to the public API by default — no VPN, no tunnel.

The product is called nesly

v0.2.0
  • One brand everywhere: nesly / nesly.net, module and API namespaces included.

Signed macOS binaries

v0.1.1
  • The darwin CLI builds are signed and notarized; brew install neslyhq/tap/nesly.

First release

v0.1.0
  • Deploy static sites and container applications from the CLI; every project gets a live URL with TLS.
  • Managed PostgreSQL per project, with a real tenancy fence.
  • Applications run sandboxed (gVisor) with resource bounds; every image is scanned on push.
  • Sign up with e-mail or social login; subscriptions with a 30-day trial.
  • Usage metering with daily charts in the console.