# parse-rust
**Parse Server, rebuilt in Rust.**
An embeddable, Rust-native implementation designed for wire and MongoDB compatibility with
[Parse Server](https://github.com/parse-community/parse-server).
**Unofficial.** Not affiliated with, endorsed by, or sponsored by Parse Community. Parse and Parse
Server are trademarks of their respective owners.
## Status: work in progress
**This is an early implementation, and the intent is to finish it.** Parse Server is a large
surface and parse-rust covers a slice of it. 0.1.0 showed that an unmodified Parse SDK could sign
up, log in, create an object, update a field, query it back and fetch it by id against MongoDB,
and that the rows it wrote were interchangeable with parse-server's on the same database.
0.2.0 is the authorization milestone, and it changes what the database can be. Roles resolve,
class-level permissions are evaluated, pointer permissions narrow queries, protected fields are
stripped, and the sessions, roles and join tables it writes are the rows parse-server reads from
the same database. 0.1.0 could talk to a Parse client; 0.2.0 can be pointed at a Parse database.
0.2.1 closes the stock-configuration gaps found while testing that milestone: the master key is
limited to loopback unless `masterKeyIps` says otherwise, CLP-declared default ACLs are applied on
create, and falsy or object-shaped `_User` ACLs no longer leave the row public or remove its
owner's access. Everything else is ahead of that, not behind it.
**Not production software.** Single node, MongoDB only, no security guarantee, and most of Parse's
surface is absent. Do not point it at data you care about.
The project exists to reach parity, not to demonstrate a subset. Each release should move a
subsystem out of the second list below and into the first, and `CHANGELOG.md` records what moved.
### What works today
| Objects | `POST`, `GET`, `PUT`, `DELETE` on `/classes/:class`, and `GET` with `where`, `limit`, `skip`, `order`, `keys`, `excludeKeys`, `count` |
| Queries | `$or`, `$and`, `$nor`, `$regex` with `$options`, `$all`, `$relatedTo`, and `include` with dotted paths |
| Users | `POST /users` (signup, bcrypt), `POST /login`, `GET /users/me`, `POST /logout` |
| Sessions | `_Session` rows in upstream's format, surviving a restart and readable by parse-server. `/sessions` with `me`, list, get and delete |
| Roles | `_Role` with its `users` and `roles` relations, the five `/roles` verbs, and transitive role graph expansion |
| Access control | Object ACLs, class-level permissions with pointer permissions and default ACLs, and `protectedFields` |
| Schema | Classes and fields created by first write with types inferred, plus the full `/schemas` API and `/purge`, master-key only |
| Relations | `_Join` tables, `AddRelation` and `RemoveRelation`, and constraints on a `Relation`-typed field |
| Writes | The atomic update operations: `Increment`, `Add`, `AddUnique`, `Remove`, `Delete` |
| Batch | `/batch` with per-operation results and upstream's error shape |
| Types | Pointer, Date, Bytes, GeoPoint, File, Polygon, Relation and the update operations, encoded as upstream encodes them |
| Errors | Upstream's numeric codes, messages and both error envelopes |
| Transport | The JavaScript SDK's `POST`-everything form, normalized before routing |
| Server | `GET /serverInfo`, `GET /health`, the master key gate with source-address filtering, client-key validation |
| Browsers | Upstream's CORS headers on every response including errors, and an `OPTIONS` preflight answered directly. `allowOrigin` and `allowHeaders` are configurable |
### What is not there yet
LiveQuery, Cloud Code and triggers, files, push, aggregate, GraphQL, `$inQuery`, `$notInQuery`,
`$select`, `$dontSelect`, geo and `$text` queries, password reset, email verification, auth
adapters, MFA, account lockout, password policy, rate limiting, idempotency, and PostgreSQL. Of
the `_User` routes, `/users/:objectId` does not exist and `/classes/_User` refuses an ordinary
client's create and delete, so `signUp`, `logIn` and `user.save()` on an existing user all work,
while creating or deleting a user outside `POST /users` does not.
Three of those absences are not inert, and matter before you try anything against real data:
- **Retried writes duplicate**, because idempotency is not implemented. This matches upstream's
default configuration, where the feature is off unless paths are configured.
- **Query constraints that are not implemented are refused, not ignored.** A request using
`$inQuery`, `$select`, geo or `$text` gets an error naming the operator. That is deliberate: a
silently dropped constraint broadens a result set, which is an authorization failure rather than
a missing feature. Code written against parse-server will fail loudly here rather than return
too much.
- **Nothing is cached.** Every request reloads every schema and role expansion issues one query
per level of the graph. Correct and slow, deferred on purpose because a cache's staleness window
decides how long a revoked permission keeps working.
`CHANGELOG.md` carries the full list, including the deliberate differences from upstream.
### Where this is going
Roughly in order. Each step should be gated on the upstream spec files for that subsystem passing,
not on the code existing, which is why the first item is the instrument rather than a feature.
1. **The conformance harness.** Parse Server ships an executable specification, and nothing runs it
against parse-rust yet. Until that exists, every claim made here rests on hand-written
differential runners that check what someone thought to check. This is the largest gap in the
project and it is judged on its own, not bundled with a feature.
2. **Auth and users.** Password reset, email verification, auth adapters, MFA, account lockout,
password policy.
3. **Triggers.** A `TriggerHost` trait with native Rust triggers and a webhook host. Existing
`main.js` cloud code runs in a Node sidecar reached over that same webhook protocol, so
JavaScript is a compatibility path rather than a requirement.
4. **Realtime and files.** LiveQuery and its pubsub, the files adapter, GridFS.
5. **Push, aggregate, hooks, pages, security checks.**
6. **GraphQL**, last: the largest surface and the smallest share of real usage.
0.1.0, 0.2.0 and 0.2.1 are done; `CHANGELOG.md` says what each one actually landed.
PostgreSQL is a first-class planned backend rather than an afterthought. The storage trait is
shaped by two backends today even though only one is implemented, on the principle that a trait
built against a single backend bakes that backend's assumptions in.
## What parity means
The contract is **wire compatibility**, not source fidelity. An unmodified Parse SDK pointed at
parse-rust should behave exactly as it does against parse-server: same routes, same JSON shapes,
same error codes, same header semantics.
The requirement is semantic identity, with exact values wherever a client depends on them. Error
codes and messages, field names, field ordering within an object, date encoding, `objectId` shape
and the `_SCHEMA` type strings are exact, because SDKs and mixed fleets read them. JSON
whitespace, header ordering and transport framing are not, and no SDK can observe them.
Where idiomatic Rust and a literal port disagree, idiomatic Rust wins, as long as nothing a client
can observe changes. Internal structure is free. The external surface is not.
That contract is why the tests look the way they do. Everything that touches upstream behavior is
checked against upstream rather than against someone's reading of it: the ECMAScript number
formatter against Node, bcrypt in both directions against the module parse-server loads, the
Parse/BSON transform against upstream's own `MongoTransform`, the `_SCHEMA` type strings against
what parse-server actually writes into MongoDB, and the acceptance gates against a running
parse-server.
## Quick start
Requires a stable Rust toolchain and a MongoDB you can write to. Anything 7.0 or later; a single
node is fine, no replica set needed.
Install the server:
```
cargo install parse-rust-cli # installs a binary named `parse-rust`
```
Or embed it, which is the primary way this is meant to be used, since native Rust triggers and
adapter registration both require the deployment to compile its own binary:
```
cargo add parse-rust-server parse-rust-mongo
```
Either way it is configured the same:
```
PARSE_SERVER_APPLICATION_ID=your-app-id \
PARSE_SERVER_MASTER_KEY=a-long-random-secret \
PARSE_SERVER_DATABASE_URI=mongodb://127.0.0.1:27017/parse_rust_demo \
PORT=27800 \
parse-rust
```
From a clone rather than the registry, `cargo build --release` puts the same binary at
`./target/release/parse-rust`.
The application id and master key are **required and have no defaults**. The master key bypasses
every access control, so a server that starts without one configured would answer to whatever
value the documentation happened to suggest. Refusing to start is the safer failure.
It prints the address it bound:
```
parse-rust listening on http://127.0.0.1:27800
```
Set `PORT=0` to bind an ephemeral port and read the actual one off that line. That is what every
test does, so parallel runs never collide, and it also lets parse-rust run beside a real
parse-server. The default is 27800 rather than Parse's 1337 for the same reason.
It listens on loopback unless told otherwise. `PARSE_SERVER_HOST` is upstream's option name and is
honored, but the default here is `127.0.0.1` rather than upstream's `0.0.0.0`, so a half-built
server cannot end up on a network by accident. Set it to `0.0.0.0` in a container.
### Configuration
Environment variables only for now. Upstream has roughly 292 options and this is the slice that
has behavior behind it; the names are upstream's, so they carry over.
| `PARSE_SERVER_APPLICATION_ID` | none | **required** |
| `PARSE_SERVER_MASTER_KEY` | none | **required** |
| `PARSE_SERVER_MASTER_KEY_IPS` | `127.0.0.1,::1` | comma-separated IP addresses or CIDR ranges allowed to use the master key |
| `PARSE_SERVER_DATABASE_URI` | `mongodb://127.0.0.1:27017/parse` | |
| `PORT` | `27800` | `0` binds an ephemeral port and prints it |
| `PARSE_SERVER_HOST` | `127.0.0.1` | upstream defaults to `0.0.0.0`; set that in a container |
| `PARSE_SERVER_MOUNT_PATH` | `/parse` | |
| `PARSE_SERVER_JAVASCRIPT_KEY` | unset | if set, non-master requests must present a client key |
| `PARSE_SERVER_REST_API_KEY` | unset | same |
| `PARSE_SERVER_SESSION_LENGTH` | `31536000` | seconds; one year, as upstream |
| `PARSE_SERVER_EXPIRE_INACTIVE_SESSIONS` | `true` | `false` issues sessions with no `expiresAt` |
| `PARSE_SERVER_ALLOW_CUSTOM_OBJECT_ID` | `false` | lets a client choose its own `objectId` on create |
| `PARSE_SERVER_ENABLE_SANITIZED_ERROR_RESPONSE` | `true` | whether a denial tells the client why |
| `PARSE_SERVER_PROTECTED_FIELDS` | `{"_User":{"*":["email"]}}` | JSON, as upstream's option takes |
| `PARSE_SERVER_PROTECTED_FIELDS_OWNER_EXEMPT` | `true` | an owner reads its own protected fields |
| `PARSE_SERVER_PROTECTED_FIELDS_SAVE_RESPONSE_EXEMPT` | `true` | a save response is not filtered |
| `PARSE_SERVER_REQUEST_COMPLEXITY_BATCH_REQUEST_LIMIT` | `-1` | unlimited; master and maintenance bypass it |
| `PARSE_SERVER_DATABASE_CREATE_INDEX_ROLE_NAME` | `true` | the unique index on `_Role.name` |
| `PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION` | `false` | whether a non-master caller may create a class |
| `PARSE_SERVER_ALLOW_ORIGIN` | `*` | comma-separated; an explicitly empty value allows no origin |
| `PARSE_SERVER_ALLOW_HEADERS` | unset | comma-separated, added to upstream's default list |
The client keys are all-or-nothing, as upstream: configure none and none is required; configure
any one and every non-master request must present a matching key.
The master key is accepted only when the connection's peer address matches
`PARSE_SERVER_MASTER_KEY_IPS`. Forwarding headers do not change that address. A container or a
deployment behind a load balancer therefore has to list the address or CIDR range the server
actually sees, not the original client's address. The environment value is not whitespace-trimmed,
and an empty value is a startup error rather than "allow none", matching upstream.
An unparsable value is a startup failure rather than a fallback to the default. Several of these
are security defaults, and a typo in `PARSE_SERVER_EXPIRE_INACTIVE_SESSIONS` must not quietly
produce sessions that never expire.
```
curl -s http://127.0.0.1:27800/parse/health
{"status":"ok"}
```
### With the JavaScript SDK
Nothing about the SDK is modified or specially configured.
```js
const Parse = require('parse/node');
Parse.initialize('your-app-id');
Parse.serverURL = 'http://127.0.0.1:27800/parse';
const user = new Parse.User();
user.set('username', 'alice');
user.set('password', 'hunter2');
await user.signUp();
const Note = Parse.Object.extend('Note');
const note = new Note();
note.set('title', 'hello');
note.set('views', 1);
await note.save();
note.set('views', 2);
await note.save();
const q = new Parse.Query(Note);
q.equalTo('title', 'hello');
console.log((await q.find()).map(o => o.get('views'))); // [ 2 ]
```
The SDK does not send the REST API the documentation describes. Every call is a `POST` with a
`text/plain` body, and the method, the credentials and the query parameters all travel inside that
body so a browser never sends a CORS preflight. parse-rust normalizes that the way upstream does.
You do not need to know this to use it, but it is why "point the SDK at it" is a stronger claim
than "the endpoints exist".
Avoiding the preflight is only half of what a browser checks, so parse-rust also sends upstream's
CORS headers on every response, including error responses, and answers an `OPTIONS` preflight
directly. `PARSE_SERVER_ALLOW_ORIGIN` and `PARSE_SERVER_ALLOW_HEADERS` take comma-separated lists;
the defaults are `*` and the twelve headers a Parse SDK sends.
### With curl
```
BASE=http://127.0.0.1:27800/parse
H=(-H 'Content-Type: application/json' -H 'X-Parse-Application-Id: your-app-id')
curl -s "${H[@]}" -X POST "$BASE/users" -d '{"username":"alice","password":"hunter2"}'
curl -s "${H[@]}" -X POST "$BASE/login" -d '{"username":"alice","password":"hunter2"}'
T='r:…' # the sessionToken from above
curl -s "${H[@]}" -H "X-Parse-Session-Token: $T" -X POST "$BASE/classes/Note" -d '{"title":"hello","views":3}'
curl -s "${H[@]}" -H "X-Parse-Session-Token: $T" -X PUT "$BASE/classes/Note/<id>" -d '{"views":4}'
curl -s -G "${H[@]}" -H "X-Parse-Session-Token: $T" "$BASE/classes/Note" --data-urlencode 'where={"title":"hello"}'
```
Note what a login response does *not* contain: no `password`, and no `_hashed_password`. Every
response is filtered so that no `_`-prefixed key can reach a client.
The class does not need to exist before the first write. That write creates it and infers each
field's type, and every later write is checked against those types:
```
curl -s "${H[@]}" -X POST "$BASE/classes/Note" -d '{"title":42}'
{"code":111,"error":"schema mismatch for Note.title; expected String but got Number"}
```
That message is byte-for-byte upstream's. Error codes and messages are API here, not diagnostics.
## Architecture
A Cargo workspace whose crate boundaries mirror upstream subsystem boundaries, so a change
upstream maps to an obvious place here.
| [`parse-rust-server`](https://crates.io/crates/parse-rust-server) | [docs](https://docs.rs/parse-rust-server) | Library. Router, middleware, config. **What you depend on.** |
| [`parse-rust-cli`](https://crates.io/crates/parse-rust-cli) | [docs](https://docs.rs/parse-rust-cli) | Installs the `parse-rust` executable, and nothing else. |
| [`parse-rust-core`](https://crates.io/crates/parse-rust-core) | [docs](https://docs.rs/parse-rust-core) | Parse types, error codes, JSON encoding. No I/O. |
| [`parse-rust-schema`](https://crates.io/crates/parse-rust-schema) | [docs](https://docs.rs/parse-rust-schema) | Field types, inference, validation, the `_SCHEMA` storage format. |
| [`parse-rust-storage`](https://crates.io/crates/parse-rust-storage) | [docs](https://docs.rs/parse-rust-storage) | `StorageAdapter` trait and the query AST adapters lower. |
| [`parse-rust-mongo`](https://crates.io/crates/parse-rust-mongo) | [docs](https://docs.rs/parse-rust-mongo) | MongoDB adapter and the Parse/BSON transform. |
| [`parse-rust-rest`](https://crates.io/crates/parse-rust-rest) | [docs](https://docs.rs/parse-rust-rest) | The read and write pipelines. |
| [`parse-rust-auth`](https://crates.io/crates/parse-rust-auth) | [docs](https://docs.rs/parse-rust-auth) | Password hashing, `_Session`-backed sessions, role graph expansion. |
The project, the repository, this README and the executable are all **parse-rust**. Only the
Cargo package names carry a qualifier, because the normalized registry name `parse-rust` is
already occupied on crates.io by [an unrelated string-parsing
crate](https://crates.io/crates/parse_rust), and crates.io treats `parse-rust` and `parse_rust` as
one name.
**A library first, with a thin binary on top.** Native Rust triggers will require a deployment to
compile its own binary, and adapters are registered through a builder rather than resolved from a
module name, so the primary artifact is something you link against.
**The storage trait is shaped by two backends, not one.** It takes a query AST rather than a Mongo
query document, because handing a Mongo document to a SQL backend means writing a Mongo
interpreter in SQL. The rule: if a method can only be implemented sensibly for one backend, the
trait is wrong.
**Atomicity is part of that trait's contract.** If correctness depends on a predicate and a
mutation happening as one unit, that unit is one trait method. The method carries both the
precondition and the complete delta, and callers must not reconstruct either from state they read
earlier. No backend can restore atomicity after the interface has split it across calls. Most of
the schema-write defects fixed during 0.2.0 were fixed by changing the trait rather than the call
site, and the shape to watch for is a method that takes a whole schema and writes all of it: that
signature cannot express "change only this", so every caller of it is one interleaving away from
undoing another writer's change.
## Testing
```
tools/test.sh # everything available on this machine
tools/test.sh --quick # skip steps needing node, MongoDB or an upstream checkout
```
Steps that need more than a Rust toolchain skip with a stated reason rather than silently passing.
Several suites compare against a real parse-server checkout. They expect it as a sibling directory
(`../parse-server`), or wherever `PARSE_SERVER_ROOT` points. The five acceptance gates are:
- **Gate A** drives the whole flow through the unmodified `parse` npm SDK, including ACL round
trips, cross-user read and write isolation, and rejection of invalid session tokens.
- **Gate B** points parse-rust *and* a real parse-server at the same database, writes with each,
reads with the other, and compares the stored BSON types rather than just the values. Values
alone would not catch the Int32-versus-Double rule, since both read back as the same JavaScript
number.
- **Gate C** drives the authorization model through the same unmodified SDK against both servers
and compares the answers: roles, class-level permissions, pointer permissions and protected
fields.
- **Gate D** points both servers at one database and checks that neither rewrites the other's
`_Session`, `_Role` or `_Join` schema, that a CLP block survives an ordinary write by either,
and that a failed write leaves the same `_SCHEMA` state behind under both.
- **Gate E** boots parse-rust and parse-server at stock and configured settings, then exercises
both from loopback and a second source address. It compares master-key IP filtering,
CLP-declared default ACLs and the `_User` identity cases fixed in 0.2.1.
Each gate carries an assertion floor and fails if it runs fewer checks than it declares, so a gate
cannot quietly stop testing anything while still reporting green.
Gates B, C, D and E also run against a real parse-server, so a failure there means parse-rust
diverged rather than that an expectation was invented. Gate A's assertions hold against
parse-server too, but it is executed only against parse-rust.
What the gates do not do is check combinations. They walk stories, and the defects found late in
0.2.0 all came from composition: a server-imposed predicate meeting a client-supplied one, an
authorization decision keyed on the wrong one of two similar values, metadata written without the
state it describes. A hand-written runner does not think to write those, which is the argument for
running the upstream spec suite rather than for adding more gates.
## Reference implementation
Parse Server 9.10.1-alpha.6 is the target, pinned at the commit recorded in `PIN`. Claims about
upstream behavior in this codebase carry a `File.js:LINE` citation against that pin rather than a
recollection, because Parse's behavior is under-documented and the edge cases are exactly where
the surprises are.
```
git -C ../parse-server show $(awk '/^parse-server /{print $3}' PIN):src/Controllers/DatabaseController.js
```
Comments marked `UPSTREAM-QUIRK:` record behavior that is reproduced deliberately even though it
looks wrong. Those are load-bearing. Removing one because it reads oddly is how a
wire-compatibility bug gets reintroduced in good faith.
## Differences from upstream
Not every difference is a defect, but an undocumented one is. Deliberate differences are recorded
in the code at the point where they apply, with the upstream citation showing what is being
diverged from, and summarized per release in `CHANGELOG.md`.
One standing rule: bug-compatibility does not extend to upstream behavior that leaks data. Where a
quirk is wire-visible but the visible behavior is unauthorized disclosure, parse-rust implements
the safe behavior and says so at the call site.
## Contributing
See `CONTRIBUTING.md`. Contributions arrive under Apache-2.0 by way of section 5 of the licence,
so there is nothing extra to sign.
## License
Apache License 2.0. See `LICENSE`.
parse-rust is a **derivative work** of Parse Server rather than an independent clean-room
implementation: its behavior, wire protocol, error codes and configuration surface are derived
from the Parse Server source, which is licensed under Apache 2.0. `NOTICE` records that and is
part of the licensing terms rather than a courtesy.
Parse and Parse Server are trademarks of their respective owners. This project is not affiliated
with, endorsed by, or sponsored by Parse Community.