This action will delete this post on this instance and on all federated instances, and it cannot be undone. Are you certain you want to delete this post?
This action will delete this post on this instance and on all federated instances, and it cannot be undone. Are you certain you want to delete this post?
This action will block this actor and hide all of their past and future posts. Are you certain you want to block this actor?
This action will block this object. Are you certain you want to block this object?
Are you sure you want to delete the OAuth client [Client Name]? This action cannot be undone and will revoke all access tokens for this client.
Are you sure you want to revoke the OAuth token [Token ID]? This action cannot be undone and will immediately revoke access for this token.
#crystallang 91 hashtags

It is said that there are only two hard things in computer science: cache invalidation and naming things. The story goes: you have something that is expensive to compute, so you compute it once and then you cache it and use the cached value in the future. But the inputs to that computation change, and so the cached value grows stale. You have to decide when and how to recompute that value.
In Ktistec, presenting accurate tag counts is expensive because not every tagged post counts. Posts are deleted, actors are blocked. My own drafts don't count, but when they're published they do. A post tagged with the same hashtag more than once, must count as one. And tag cardinality is not uniform: #3dprinting has hundreds of thousands of posts, others have one or two. Even with indexes, there is no single query that counts all cases in an acceptable amount of time.
So I reached for a cache, counted once and then cached the count. Because I didn't want to maintain adjustments from every place in the code that changed something that touched the count, I settled for eventual consistency and recomputed counts after every server restart.
As it turns out, that's not good enough. On a server with reasonable traffic, an event that affects some tag's count happens every few hours. Days or weeks later there is significant drift. Worse, the implementation didn't recompute on first read, it recomputed on first write (a new tagged object arrives).
This release fixes all that. Counts are still eventually consistent, but all counts are recomputed in a regular background task, so they really are eventually consistent, and care was taken in constructing the query to minimize database (read) locking to ~100-200msec.
Is it better? Yes! Is it perfect? Probably not. Cache invalidation is hard.
Here's the full changelog for this release:
Added
Fixed
Changed
Removed
idx_relationships_type database index.In the next release, I'm going to fix a few bugs in the Mastodon-compatible API. These require an internal redesign, so I've held off until a few other things were out of the way. And I'm turning my attention to reading and better tools for surfacing and finding interesting content.

I really enjoy optimization. Release v3.5.0 of Ktistec doesn't drop significant new features, but it does deliver a ~15% smaller executable and significantly faster queries on anonymous endpoints. The two are intertwined.
The size reduction comes from replacing a poorly designed, custom rules engine with a materialized view layer that uses SQL to define membership in a collection. The rules engine worked well enough but required a lot of supporting code to present rules as a DSL (Domain Specific Language) over the domain objects in ktistec. The driving realization was that SQL is a DSL and membership in a collection is just a query and domain objects are just rows. Voil脿!
Query performance improvements came from using this new view layer to materialize two very popular but expensive-to-query views: the instance's public timeline and public hashtag pages. Because both are public pages they receive more traffic than internal pages.
The problem with the original queries was that performance was not uniform. Querying for posts with popular tags was okay. Querying for posts with sparse tags was very slow. I could have added more indexes, but that's its own cost. After the change, endpoints all respond in a consistent ~10msec timeframe and the CPU barely registers when a crawler hits. (I don't want to make things easier for bots, but I don't want to pay a tax for their activity either鈥攁sk me about my new nginx configuration.)
Here is the full changelog:
Added
max-id and min-id pagination links on web pages.Fixed
Changed
Removed
school dependency; replaced by activity processors and materialized views.openssl_ext dependency; vendored in.There are still a few slow queries. In the next release I'm going to see if I can get everything under 10msec, and maybe release a new feature, too. 馃殌

This release fixes a small number of bugs found in recent releases.
The full changelog:
Fixed
header and header_static images are always present.replies collection for local objects.Changed
Removed
This release fixes a hard-to-exploit but potentially server-crashing bug. If you're running v3.3.9 or v3.4.0, you should upgrade.

The biggest change in release v3.4.0 of Ktistec is cursor-based pagination for all web-navigable collections (timeline, notifications, etc.). Offset-based pagination will be removed completely in the next release.
Offset-based (e.g. page/size) pagination works well on collections that don't change. But, what does "the second page" contain in a dynamic timeline? Support for cursor-based pagination is required by the Mastodon-compatible API, but has been a desirable feature for quite a while.
While updating queries to paginate by cursor, I also made performance improvements to the queries themselves, as mentioned elsewhere. Scrapers and bots have already adapted鈥攕ort of. I now see odd hybrid requests in the log like /tags/xyz?page=7&min_id=123. Overall CPU usage under normal load is now sitting at 0-1%.
Here is the full changelog for the release:
Added
/api/v1/timelines/tag/:hashtag endpoint.Fixed
published rather than id.Changed
Ktistec::Network.get.Removed
Enjoy!

Release v3.3.9 of Ktistec continues the security hardening work from recent releases, with further progress on the Mastodon-compatible API.
Of note: all network connections now go through a new Ktistec::Network module. This allows Ktistec to limit the size of HTTP bodies it reads, on both inbound and outbound requests, and ensures it only opens connections to valid remote IP addresses.
Here's the full changelog:
Added
Fixed
Changed
Ktistec::Network.As always, it's worth upgrading for the security fixes!

This release continues my focus on security instead of new features. As I wrote earlier this week, I rebuilt the template framework Ktistec uses with type safety as a central principle. What does that mean?
Imagine that you have an instance of a String that holds federated data. Where can you safely render that in a browser, and what operations (sanitization, escaping, etc.) do you need to do first?
The only way to answer that is to look carefully at the lineage of that data: where it came from, how it was stored, how it was transformed, and where it's rendered. A name holds text; an href or src attribute holds a URL. If you want to render a name inside an HTML element you should HTML escape it. You should escape href and src, too, but the escaping rules for URLs are slightly different from the HTML rules. It's easy to make mistakes.
Ktistec uses four "safe" types to express the contracts:
SafeHTML: A String wrapper marking HTML markup safe to emit raw into HTML data slots (text content, between tags).
SafeAttrValue: A String wrapper marking a value safe to emit raw inside a double-quoted HTML attribute (attr="..."), other than URL or event-handler slots.
SafeURI: A String wrapper marking a URL safe to emit raw into a URL attribute slot (href, src, action, etc.).
SafeJSON: A String wrapper marking JSON output safe to emit raw into the body of a <script type="application/json"> block.
Using the wrong type at a call site is either a compile-time error, or it triggers automatic sanitization of the underlying string value.
Here's the full changelog:
Added
WebFinger and HostMeta client shards.Fixed
I have at least one more cleanup pass to do, and then I'll turn my attention back to the Mastodon-compatible API and a few features I've been looking forward to鈥攍ike scheduled posts.

One of the nice benefits of working on an open source project is that you can scratch an itch for as long as you feel like scratching. A game I like to play while scratching is called "what invalid states can I make unrepresentable" using the type system.
ktistec uses a template language for views and partials. In its original form, it allowed a programmer to use = to escape an untrusted value or == to render it unescaped. You might want to escape an actor's name property because a name should never contain HTML but you might want to sanitize an object's content property and then render it without escaping because the body of a post can contain HTML.
The problem is you have to remember the rules and never make a mistake. If you accidentally type == actor.name you've just created a potential cross-site scripting (XSS) vulnerability!
ktistec's template language now makes it much more difficult to screw up.
There's now only = syntax and by default it escapes everything. The only way to get it to emit a string without escaping is to wrap it in SafeHTML. The sanitize helper sanitizes HTML and then wraps it for you. Other common helpers (e.g. path construction helpers) do the same.
Importantly, if you interpolate a safe value into a string, it is demoted back to a string and will be escaped unless it is explicitly wrapped again.
Unescaped HTML is still possible to construct, but it's now much harder to do so accidentally. You can't just concatenate some strings together鈥攆orgetting that one comes from an untrusted source鈥攁nd render that as unescaped HTML.

Release v3.3.7 of Ktistec fixes several bugs and introduces two enhancements.
Security is a focus in this release. Every gap in input sanitization or escaping is a potential vulnerability, and I've been systematically closing them. I am also carefully, and maybe conservatively, restricting things like supported URL schemes and uploaded file types.
The two enhancements improve compatibility with Mastodon-compatible clients. Mastodon's OAuth tokens don't expire, and Mastodon clients don't know how to handle tokens that do. Sliding expiration ensures that tokens in active use stay alive, while unused tokens eventually expire.
Here's the full changelog:
Added
/api/v1/accounts/update_credentials endpoint.Fixed
href attributes with unsafe schemes from sanitized HTML.X-Content-Type-Options: nosniff.publicKey and scrub Tag.href.I'm working on performance improvements for the next release. A rewrite of the Slang template library looks like it will cut both build time and executable size by around 10%!
馃摗 Stay tuned!

A new patch release Crystal 1.20.1 fixes some regressions and disables Kernel TLS (added in 1.20.0), due to instability.
Read more at https://crystal-lang.org/2026/04/29/1.20.1-released/

This release is a maintenance update: a few bug fixes, a security mitigation worth paying attention to, and some performance improvements for users on slow connections.
It's worth updating to pick up the SSRF (Server-Side Request Forgery) mitigation.
Fixed
Changed