โก TL;DR
- ๐ Statistics and ticket analytics work on every server now. Free servers get 7 days, nyx+ gets 30.
- ๐๏ธ Hosted transcripts expire after a year. You can shorten that to a week.
- ๐งน Removing nyx deletes your data properly. It didn't before, and I'll explain that below.
- ๐ API keys carry scopes and can be locked to specific servers. Old keys stopped working.
- ๐ซ Tickets track whose turn it is, and the queue shows you which ones are yours.
- ๐ถ There's a pricing page, a status page, and a privacy policy with real numbers in it.
Part two is the technical half. Stop after part one if that's all you need.
Part one: what changed
๐ Statistics work without nyx+ now
Server statistics and ticket analytics used to be a grey box with a padlock on it. You couldn't tell whether the feature was any good, which made it a strange thing to ask people to pay for.
Every server now sees the last 7 days. Charts, response times, the lot. nyx+ widens it to 30.
Here's the part I like. Collection was never gated, so nyx has been recording your server the whole time. Subscribing doesn't start a clock. It hands you the history you already had.
๐๏ธ Transcripts stop living forever
Hosted ticket transcripts were kept indefinitely. Nobody decided that. It's just what happens when nothing deletes anything.
They now expire after 365 days. A server owner can shorten that to as little as 7 under Tickets, then Logging. Wanting less is as legitimate as wanting more, so the dial turns both ways.
Transcripts sent to your log channel as file attachments aren't affected. Discord holds those, not me.
๐งน Removing nyx now deletes your data
This one's a bug report.
When you removed nyx from a server, I deleted the server's row and assumed the rest went with it. It didn't. Transcripts, close-time ratings, honeypot counts, saved embeds and activity stats all survived. Two tables had no link back to the server, so nothing ever came looking for them.
Fixed. Removal now clears all of it in one go. There's also a maintenance command for the data left behind by the old behaviour, and I've run it.
So if you removed nyx a while back and assumed your data went with it, it didn't. It has now.
๐ API keys can do less on purpose
The API launched two days ago with keys that inherited everything you own and every endpoint there is. That's fine for a script you wrote yourself. It's not fine for handing to a contractor.
Keys now carry scopes, picked when you create one.
guilds:read ยท stats:read ยท progression:read ยท tickets:read ยท honeypot:read ยท diagnostics:read
You can also lock a key to specific servers. Leave that blank and it covers everything you own, which is still the right default for personal use.
Scopes only ever narrow. Name a server on a key, then transfer that server away, and the key reads nothing, because ownership gets checked on every request instead of trusted from the key.
Old keys stopped working. A key made before scopes can't say what it's allowed to reach, and the only answer left would have been "everything". Make a new one on your profile.
๐ซ Tickets know whose turn it is
Every ticket carries a status now, and it updates itself from whoever spoke last.
- Awaiting first reply, until someone from your team responds
- Waiting on member, after your team replies
- Waiting on staff, once the member answers
It shows on the ticket header and in the dashboard queue. Nine open tickets stops being a wall and becomes "two of these are ours right now". Nobody sets it by hand, so nobody forgets to.
It fixed something else on the way. Inactivity warnings and auto-close only fire when the ball is with the member. A ticket sitting unanswered because your team is busy won't quietly close itself any more, which it used to, which was awful.
Tags are gone. They lived in the data model, showed up in every ticket header as "None", and nothing in nyx could set them. Status does the job they never did.
๐ฌ Ticket embeds got tidied up
Intake answers get their own embed. They used to be crammed into one field of the header, four answers maximum, each one cut at 137 characters. That gutted exactly the long answers worth reading. Now every question gets its own field and the whole answer.
The close log got rebuilt too. It was a paragraph of bold labels. It's now a summary with who opened it, who closed it, who claimed it, how long it stayed open, and how quickly the first reply came. Those last two were already being measured and thrown away.
Intake answers don't appear in the close log any more. Once a ticket is closed, what someone typed to open it is history, and the transcript already has it.
๐ถ A pricing page
nyx.cool/pricing exists. Until now you had to sign in and go hunting to find out what nyx+ cost, which is a good way to sell nothing.
It also says the thing that was hiding in 11px grey text. One subscription covers every server you own. Not one per server.
๐ A status page
nyx.cool/status shows live health for the bot, the dashboard and the API. Your browser does the checking, not my server, because a server asked whether it's up will always say yes.
There's a section on it called "What this page does not tell you". It admits the page shows live state rather than history, that "running for" isn't an uptime percentage, and that the page runs on the same machines it reports on, so a real outage takes it down instead of turning it red. A status page that hides its own limits isn't worth reading.
๐ Privacy policy with real numbers
The retention section used to say data is kept "only as long as reasonably needed", which means nothing.
It now lists periods. Transcripts 365 days. Scheduled embed records 30 days. Sessions 7 days. Caches between five minutes and an hour. API keys until you revoke them.
It also names every company involved in running nyx, and adds two sections that didn't exist before, covering where your data is processed and what third parties run in your browser.
That second one has a one-word answer now, which brings me to the next bit.
๐ค Google is out of your browser
nyx.cool and api.nyx.cool were loading fonts from Google. Every visitor's IP went to Google before a single pixel rendered, which is the sort of thing you inherit from a template and never look at again. Fonts come from our own domain now.
It's also quicker. One variable font per family, carrying every weight, beats twelve separate files fetched from someone else's domain.
๐ง Also
- The API has a machine-readable spec at api.nyx.cool/openapi.json. Point a client generator at it instead of writing request code by hand.
- Deep links into the dashboard used to dump you on the homepage with no explanation. They now tell you what you were trying to reach and offer to sign you in.
- Link previews work. Sharing a nyx.cool link in Discord shows a proper card, which it has never done, because the preview image was set to a relative path that every crawler silently ignored.
Part two: how it was built
The deletion bug
deleteGuild removed the row from guilds and trusted foreign keys to take the rest. Exactly one table had one.
plugin_data can't have a foreign key to guilds. It stores global rows next to guild rows and tells them apart with a scope column, and a constraint can't apply to half a table. guild_stats_daily just never got one. Both survived every removal, and with the owning row gone there was nothing left to search by.
The fix deletes both explicitly, before the guild row, in one transaction. Order matters here. A half-finished erasure that drops the row identifying the data while keeping the data is worse than never starting.
Cleaning up the existing orphans is a script, not a migration, because it destroys data and nobody should set that off by deploying. It reports before it deletes and breaks the count down per plugin, so you can see whether transcripts are in there before you commit to anything. It also refuses to run when the guilds table is empty. "No guilds" and "every row is an orphan" are the same query result, and one of those readings deletes everything you have.
I checked the regression test by reverting the fix and watching it fail. A test that has never failed proves nothing.
Retention
The statistics change is about four lines. The window was already a parameter, so it picks 7 or 30 from entitlement and returns the metrics either way instead of an empty array.
Ticket analytics needed more work and turned up something worse. The old code loaded every transcript the server had ever closed, one store read at a time, then threw most of them away for falling outside a 30 day window. Transcript references carry their close time, so the window gets applied before the fetching now. A server with years of tickets went from reading its whole archive on every page load to reading a month.
One consequence worth stating. Averages now describe the window rather than all time. Total closed tickets is still all-time, and it now costs zero reads, because it's just the length of the reference list.
Scoped keys
The original design deliberately stored no permissions on the key. Ownership and nyx+ both change, and a transferred server has to drop out on the next call rather than waiting for someone to remember to revoke something.
Scopes had to keep that property. So what's on a key intersects with live authority instead of replacing it. The key says what it may ask for. The control plane still decides what's actually visible. Putting a guild id on a key can never grant access to a guild you no longer own.
Scope checks run before any upstream call, so a key without the right scope costs no work and can't tell a real server from a made-up one by timing the answer.
The spec generates itself
/openapi.json is built from the same endpoint registry that renders the documentation pages. A spec maintained beside the docs drifts the first time somebody updates one and forgets the other. Now adding an endpoint adds it to the sidebar, the reference page and the spec at once, or to none of them. A test fails if the two counts ever disagree.
Response bodies are described by example rather than schema. The examples are the literals the docs already show, parsed at build time so they can't go stale. Proper schemas would have to be generated from the shared types to be worth trusting, and an example that's definitely right beats a schema that might not be.
Ticket status
The status enum shipped months ago with five values and only ever held one of them. waiting_user existed in the type and was never once assigned.
The transition is derived from who spoke last, inside the message handler that already worked out whether the author was staff and already saved state. open stays separate from in_progress because "nobody has replied yet" and "the conversation is live and it's our turn" want different responses, and the first-reply SLA only cares about the first one.
resolved came out of the enum entirely. Closing a ticket on Discord archives the channel, so resolving and closing are the same event, and a resolved-but-open ticket can't exist. Narrowing the type immediately caught a usage that a text search had missed, because it was derived rather than written out.
Not much here is new. Most of it is nyx holding less of your data, for less time, and being able to show you the numbers. ๐ค