Most headless CMS pitches sound the same. Storyblok had one line that did not: you can define your content model as TypeScript in your own repo and push it to your space with a single command.
That is a real difference. Your content structure stops being a series of clicks in an admin panel that nobody can diff, and becomes code that goes through a pull request like everything else. I wanted to know whether it held up in practice, so I built a documentation site with it.
It mostly does hold up. This is what I found, including the part where I had to retract half of my own complaints.
Not prose in markdown files. Docs as structured content. A page is a title, a category, an order, and a list of typed blocks. A callout is a block. An API endpoint is a block with typed parameter blocks inside it.
The reason that matters is simple. Markdown cannot answer questions about itself. Ask which of your endpoints still document a parameter you deprecated last quarter, and markdown gives you grep and a careful read. Typed content gives you a query.
Eleven block types, two dropdowns backed by shared lists, the whole model defined in code and pushed with one command. The site builds its own navigation from the content, because category and ordering are real fields rather than a sidebar file somebody has to remember to update.
A few things stood out enough to be worth naming.
The push is a diff, not a leap of faith. Running the push with --dry-run prints exactly what will be created, updated, or left behind before anything happens. A real push then writes a changeset file, and there is a rollback command that reads it back. For a tool that mutates shared state, that is the right default.
The types do real work. Field definitions narrow per field type, so the options available to you depend on the type you picked. There are also small helpers for conditional fields, so "hide this unless that checkbox is ticked" is a typed function call rather than hand written JSON. On their main branch this has gone further: an option that means nothing for the field type now fails to compile, with an error naming the offending key. That is not in the released package yet, which turns out to be a theme.
The codebase explains itself. Reading their source, I kept finding comments that explain why a decision was made, not just what the code does. One comment told me a feature was deliberately type level only and explained what to use instead. That is rarer than it should be, and it saved me hours.
The API errors are precise. When a push failed, the message named the exact field and the exact problem. No guessing.
Building it, I collected friction. The CLI died behind a proxy with an error that said nothing useful. A validator waved through a schema the server then rejected. A dropdown sat empty after a push that reported success. The published documentation described a component that the installed package no longer exports.
I wrote ten of these up as a troubleshooting page on the site itself. It felt like the most useful page on there.
Here is the uncomfortable part. Of those ten, I had properly tested about four. The rest came from reading comments in the library's type definitions, or from reasonable assumptions I never verified. So I went back and tested each one against a real space.
Three claims were vendor comments I had simply repeated. Two of those turned out to be about the editor interface, which I could not test through the API at all. My test record saved and published perfectly happily with a supposedly blocking field left empty, which told me the enforcement is not where I had assumed.
One claim was mine and it was wrong. I had written that the validator should reject block names containing spaces. So I pushed a component named Has Spaces And Capitals. Storyblok accepted it without complaint. The validator was right and I was not.
And the one I felt most sure about, a field restriction that silently did nothing, was real in the published package but had already been fixed upstream days earlier. Their main branch was well ahead of what npm had. If I had filed that report, I would have been explaining a bug to the person who already fixed it.
Both surfaced while I was correcting the others, which I did not expect.
The first is small and odd. Through the Management API, create a record with a number field set to 7 and it works. Update that same record with the same 7 and you get a 422 saying the value must be a string. Same field, same value, different verb.
The second matters more. The Management API does not validate content on create. I sent a record with a required field left empty and a badly formatted number, and it returned 201 and stored it. The identical payload sent as an update returned a 422 on both counts. So a script can create records that no later update can touch until the content is repaired, with no warning at the moment of creation. I only noticed because my own seeding script broke on its second run.
One finding was clean enough to fix rather than report.
Storyblok has a command that checks your content model before you upload it, and it is thorough. It catches typos in references, conflicting settings, missing names. What it did not check was whether a field's type is a real type. So you could write type: "banana", the checker says all clear, and the upload then fails.
Think of it as a spellchecker that verifies your grammar and your citations but never checks whether the words are real words.
The fix is small. Hand the checker the list of real types and let it complain. The wrinkle is that the list existed only as a TypeScript type, and types vanish when code runs, so there was nothing to compare against at runtime. I wrote the list out as data and added a check that fails their build if the two ever drift apart.
About a hundred lines with tests. I removed the fix and confirmed the new test failed, then restored it and confirmed it passed, because a test that passes either way is decoration. It is open as a pull request now.
The tooling is good and moving fast, and that speed is exactly why half my list was stale. Anything you test against the published package may already be fixed on main.
The rest of the lesson is duller and more useful. A comment in a library's type definitions is not a test result. Neither is a documentation page. Neither is something you noticed once while tired and filed away as broken.
If you are about to tell someone their software is broken, check first. Check against their main branch, not the release you happen to have installed. Check whether somebody already fixed it. And check your own claims hardest, because the one I was most certain about was the one I had to retract.
I nearly sent a bug report for something already fixed. The only reason I did not is that I stopped and asked myself whether I was actually sure.