I'm a full-stack developer in Nairobi, and I recently took on two small tasks for Voxgig: build an open-source SDK with their SDK generator, then write a critical review of Aontu, the language the generator is built on.
This post covers both. First, what I built. Then what I found when I tried to break Aontu.
What I built
I used the Voxgig SDK generator to produce a TypeScript client for PokéAPI. The model behind it is 102 .aontu files across entity/ and flow/, 11,622 lines in total.
The full project is on GitHub: github.com/Enos2/my-sdk-work
The review
I did not choose Aontu. I met it because the Voxgig SDK generator describes an API in .aontu files, and I used the generator to build a TypeScript client for PokéAPI. Afterwards I took the Aontu 0.76.1 that the generator installs and spent some time trying to break it.
The site describes it as a language for the entities in a system, the types they must have and the relations that must hold. Schema and data share one notation, and statements combine by unification: compatible statements narrow each other, contradictory ones are an error.
What works
The example from the front page runs as published:
spec: type({ service: { owner:string tier: *1 } }) services: { &: $.spec.service } services: api: owner: "platform" services: auth: { owner:"identity" tier:3 } api picks up tier: 1 from the default and auth keeps its 3. The &: line applies the schema to every entry in services, including entries added later in the file.
The error messages stood out. Two values for one key:
server: { port: 8080 } server: { port: 9090 } [aontu/scalar_value]: Cannot unify values at path $.server.port Cannot unify value: 9090 with value: 8080 --> conflict.aontu:2:17 1 | server: { port: 8080 } 2 | server: { port: 9090 } ^ value was: 9090 Above that excerpt sits a short explanation with worked examples, and the process exits with status 1. Two unrelated conflicts in one file produced two errors in one run, so there is no fix-and-rerun loop. Help works offline: aontu help language prints a grammar card and aontu explain closed explains an error code without a browser.
The typo that passes
spec: type({ service: { owner:string tier: *1 } }) services: { &: $.spec.service } services: api: { owner: "platform", tire: 3 } This evaluates without complaint and emits "tier": 1 and "tire": 3 side by side. Struct types are open by default. Wrapping the schema in close() fixes it:
spec: type({ service: close({ owner:string tier: *1 }) }) Now the same file fails with [aontu/closed] at $.services.api.tire, explained as "Cannot add to closed structure", with the caret on line 3.
The site pitches Aontu as a set of guardrails for coding agents. A misspelled or invented key is an easy mistake for a model to make, so a default that accepts it leaves the guardrail off for anyone who has not read about close(). JSON Schema is also open by default, which makes Aontu consistent with the format most people know. CUE, the other unification-based configuration language, closes definitions by default. I would want closed structs by default, or at least a project-wide switch.
The error that points at the wrong line
spec: type({ service: { owner:string tier: *1 } }) services: { &: $.spec.service } services: api: tier: 2 The author of line 3 forgot owner. The output:
[aontu/mapval_no_gen]: Cannot resolve value at path $.services.api.owner This value was present after unification, and cannot be generated because it is not a literal value. --> missing.aontu:1:31 1 | spec: type({ service: { owner:string tier: *1 } }) ^ key owner value was: string The caret lands on the schema, and the explanation describes the evaluator's state instead of the mistake. Compared with the conflict errors above, this one needs translating. A message like "services.api is missing required field owner" would do.
What the model files contain
My PokéAPI model has 102 .aontu files in entity/ and flow/, 11,622 lines in total. I searched them for type(, close(, &:, $. references and * defaults. None appear. The only language feature present is the @" include, in the index files. An entity file looks like this:
fields: { id: { h: "Id" n: "id" r: true ro: true t: "`$INTEGER`" } } That is JSON with unquoted keys, comments and includes. The schema that gives it meaning is a 290-line file inside @voxgig/sdkgen. The one-letter keys are the generator's choice. Aontu evaluated the whole model into about 350 KB of JSON in 2.9 seconds, so speed is fine. The result is that a person who uses Aontu only through a generator never meets the parts the site advertises.
Rough edges
Running the CLI directly on the model printed a warning that an include resolved outside the entry root and that "a future release will deny this by default". The SDK model includes files from node_modules, so a future upgrade will need the --trust system flag or a change in the generator.
The generated AGENTS.md says literal disjunctions such as 'prod' | 'peer' | 'dev' are fragile and should be avoided. I tried one with a valid value, an invalid value and no value at all. The valid one resolved, the invalid one produced an empty error showing both source locations, and the unresolved one produced disjunct_no_gen with advice to add a * preference. I could not reproduce any fragility. Either it needs a more complicated setup or the warning is out of date, and a reader cannot tell which.
The site I read documented 0.73.0 while my install was 0.76.1, and a 0.x version carries no stability promise.
Later lines never override earlier ones, so changing port: 8080 to 9090 further down the file is an error. The design is consistent and the site says so. Anyone coming from overlay-style configuration such as Docker Compose or Helm values may expect the later line to win.
Verdict
I would use Aontu for a hand-written model that a team wants checked in CI, with close() on every schema. I would not adopt it for data that a generator produces, because JSON plus JSON Schema gives the same checks with tooling every editor already supports. I would like closed structs by default, and the missing-field error rewritten to point at the line that left the field out.
Links
- SDK project: github.com/Enos2/my-sdk-work
- Portfolio: enos-omenda.web.app
- GitHub: github.com/Enos2
I'm Enos Omondi Omenda, a full-stack web developer based in Nairobi, Kenya. I build with React, Node.js, Express and MongoDB, work on AI backend workflows as a Back-End AI Engineering Intern at FlyRank, and I'm the founder of Narra, a live-streaming and content management platform. I enjoy exploring open-source tools and writing about what I learn along the way.
