remult: the quickstart entity opens full CRUD with no authorization
Full-stack CRUD, simplified, with SSOT TypeScript entities
At a glance
- What is it?
- remult derives a REST and realtime API from TypeScript entity classes, with validation and authorization written as code inside the class. Its first code example sets a single flag that enables read, insert, update and delete over the API with no policy attached, and its authorization example compares a field the class never declares.
- Who is it for?
- remult fits a TypeScript team that wants one entity definition to drive the API, the client and the validation rules, and who will read the authorization section before pointing it at a database. Verify four things first.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The first entity example opens read, insert, update and delete
The very first code block defines a product class with three fields and one option on the entity:
// shared/product.ts
import { Entity, Fields } from 'remult'
@Entity('products', {
allowApiCrud: true,
})
export class Product {
@Fields.id()
id = ''
@Fields.string()
name = ''
@Fields.number()
unitPrice = 0
}That single flag is not one permission, it is four: it enables read, insert, update and delete for that entity over the HTTP API, with no predicate on any of them. Per-operation flags exist and are used properly further down the page, so the finer control is available. What makes it worth flagging is ordering: the first thing a reader copies is the permissive one, and the authorization section arrives after relations. A framework that makes an unauthenticated CRUD endpoint the shortest path to a running backend is making a decision on your behalf, and the documentation does not flag it.
The authorization example compares a field the class does not declare
The security section is the best part of the page and it has a bug. The entity declares three fields: a slug, an author identifier initialised from the signed-in user, and content. The slug and the author identifier are both marked not updatable through the API, which is right. But the update predicate compares an author property, and the list prefilter filters on an author field, and neither is one of the three declared fields. So the predicate that is supposed to stop one author editing another's article is referring to a property the class does not have. Two more things about the same block are worth reading. Read access is allowed unconditionally, so the only thing restricting the list is the prefilter, and the prefilter returns an empty restriction for anyone passing a single admin role check. Fine-grained authorization here means one predicate and one filter, and both rest on names that need verifying.
Validation messages come from two different places
The validation section is where the claim about defining rules once actually pays off, and the mechanism is visible in the error shape. A rule can be a built-in validator, which takes no message and produces its own English text, or a function returning either a boolean or a string, in which case your string becomes the message. Both end up in the same map keyed by field name, alongside a summary message naming the first failure, and the backend returns that identical structure with a bad-request status. So a client sees the same object either way. The practical consequence of the two sources is that error copy is inconsistent unless you write your own message for every field, since the built-in required rule produces its own wording that will not match the tone of anything around it. A field rule also receives the whole entity, so a field can be validated against its siblings.
Live queries deliver changes rather than rows, and the credentials are a literal
Two mechanisms are worth separating. The plain fetch example calls the repository, sends it to HTTP, and hands the array to a state setter with no error handling at all, so a failed request in the quickstart produces an empty list and no message. The realtime example replaces that with a subscription, and the callback receives an object whose applyChanges member folds a change into your existing array rather than replacing it. The subscription handle is what the effect returns, so unsubscribing on unmount is handled correctly. That is a real advantage over polling, and it has a cost: client state is only correct if every change arrived in order. A dropped connection leaves stale rows rather than an error. Separately, the server example's data source is configured with a connection string containing a literal user and password placeholder, and no environment variable appears anywhere in the page.
The quickstart server starts with no port and claims one line of code
The section heading promises a single line of code and the example beneath it runs to about twenty. Fair enough, since the API really is one middleware mount, but the shape of the example invites skipping past three details. The listen call takes no port argument, so the server binds wherever the runtime puts it while the rest of the page talks about paths on a running API. The connection string is a literal. And the shared entity is imported through a relative parent path, which quietly assumes a directory layout with shared code as a sibling of the backend folder rather than inside it. The same section notes that the mount adds REST and realtime routes and that an OpenAPI specification and a GraphQL endpoint are available as options, so the middleware is doing more than the two things the heading mentions.
One query string uses three different separators
The claim is a single query syntax on both sides of the wire, and the example that demonstrates it is where the awkwardness shows. The frontend form is shown as a URL with three distinct separators in one string: a parameter whose name begins with an underscore and whose value follows an equals sign, a field predicate joined by a dot, and a sort parameter joined by a comma. Nothing is wrong with a compact query grammar, but three delimiters in ten characters is the kind of thing that surprises people writing a proxy rule or a cache key. The backend side of the same example is shown as SQL with the limit clause last, which is one vendor's dialect, while the supported list names six databases including two whose row-limiting syntax differs. A document store appears in a list otherwise of relational engines, which is the clearest signal that the data provider abstraction is doing real work.
A file named .gutattributes instead of .gitattributes
The repository root contains a file whose name is missing a letter. It is meant to be the git attributes file, with the second character dropped, and git does not recognise the name as written. The consequence is quiet and real: the line-ending normalisation and diff behaviour that file exists to configure is not configured for this repository. The same root also carries three TypeScript project configurations, and the build scripts reach three more inside the package and test directories, so a six-configuration setup is the baseline. Two of the root scripts deserve a note. The test command launches the runner with an interactive interface and coverage enabled, so the default is a browser window rather than a single pass. And the release command deletes the output directory, runs the build and then patches versions, without invoking the tests anywhere in the chain.
Nine test servers, four colours, and adapters with no example
There is one script that starts every server the project tests against, and it launches nine at once with a kill-all-on-exit flag. The names cover two server frameworks, an express server, two middleware variants, a fresh Deno server, and three meta-framework dev servers, with four colour values assigned across the nine labels so the last names reuse the first colours. It is a thorough matrix and a slightly rough interface. The examples directory tells a similar story from the other side: thirteen directories, of which one is a focused feature sample rather than an application, two are third-party table integrations rather than framework samples, and the framework coverage is Angular, Next.js in two shapes, Nuxt, React, SvelteKit, SolidStart and Vue. Three of the nine adapters the page advertises have no example at all. The build also runs a step that extracts the public API, which sits oddly beside the claim of no code generation, though the two are doing different jobs.
Editorial conclusion
remult fits a TypeScript team that wants one entity definition to drive the API, the client and the validation rules, and who will read the authorization section before pointing it at a database. Verify four things first. Treat the first entity example as unsafe by default, because the flag it sets opens four operations with no policy, and an admin interface ships with the package and is mentioned in one line. Copy the authorization pattern rather than the quickstart one, and check it for the field mismatch described above before relying on it. Decide what runs where validation happens, because the built-in validator supplies its own English message while a custom one supplies yours. And if you take the realtime path, remember it delivers incremental changes rather than refetches, so a dropped subscription leaves stale rows rather than an error.
Frequently asked questions
What is remult/remult?
A TypeScript framework where entity classes are the single source of truth for a CRUD and realtime API, a type-safe frontend client, and a backend data layer. Validation rules, field-level API permissions and record-level authorization predicates are written in the class, the same query object is used on both sides, and there is no code generation step for consumers. Data providers cover six databases and there are adapters for nine server and meta frameworks.
How do I secure a remult API?
Per-operation flags on the entity control read, insert, update and delete, and both a record-level predicate and a list-level prefilter can be written as functions that receive the signed-in user. The page's own example sets read to unconditional and relies entirely on the prefilter for the list, with the prefilter returning no restriction for anyone passing an admin role check. One detail to verify before copying it: the predicate and the filter reference a field name the class in the example does not declare.
Does remult need a separate type definition for the frontend?
No, and that is the point of the design. The entity class is shared, the same query object is written on the frontend and the backend, and the backend call receives the frontend form of it. The page adds that there is no code generation involved, which holds for consumers even though the package's own build runs a step that extracts its public API.
How does remult handle realtime updates?
With a live query that returns a subscription, and the callback receives an object whose applyChanges member folds the change into your existing array instead of handing you a fresh one. The subscription handle is what a reactive effect should return so it unsubscribes on unmount. Because state is updated incrementally, client state is only correct if every change arrived in order.
Does remult ship an admin interface?
Yes, and the page gives it a heading and a documentation link and nothing else. It appears after the relations section rather than in the feature list, and no authorization guidance is attached to it, so it is worth pairing with the entity-level permissions rather than relying on it being gated for you.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/remult-remult)