@voidwalkers/void-cli
v0.1.20
Published
CLI for Void Walkers Void
Readme
@voidwalkers/void-cli
Command-line interface for Void Walkers Void — a backend platform for serverless functions, static websites, MongoDB databases, object storage and project management.
The CLI lets you bootstrap a project, enable platform components, manage settings and configs, and deploy your code and assets.
Requirements
- Node.js 18+ (the CLI uses ES modules, top-level
awaitand JSON import attributes) - Access to the Void platform. There are no self-service accounts: an administrator uses an
admin secret to manage users, projects and per-project access tokens. To work on a
specific project you need its private/public token pair (issued by the admin and stored
in
void.json).
Installation
npm i -g @voidwalkers/void-cliAfter installation the void-cli binary is available globally.
void-cli --help
void-cli --versionQuick start
# 1. Initialize a project in the current directory
void-cli init
# 2. (optional) Enable extra components later
void-cli enable mongo
# 3. Deploy your functions and websites
void-cli deployinit creates a void.json config file in the current directory. It contains your
project slug and secret tokens, so make sure to add it to .gitignore:
void.jsonConfiguration
void.json
The project config is stored in void.json in the working directory. It is created and
maintained by the CLI (init, enable, mongo database create, storage buckets create,
websites domains set/remove, etc.). A typical file looks like:
{
"project": {
"slug": "my-project",
"tokenPrivate": "<base64 private token>",
"tokenPublic": "<base64 public token>",
"functions": {
"entryPoint": "./src/index.js",
"include": ["src"]
},
"websites": {
"items": [
{
"name": "default",
"public": "./dist",
"customDomain": { "root": "void-walkers.com", "subdomain": "shop" }
}
]
},
"mongo": {
"databases": [
{ "name": "default", "password": "...", "accessRules": {} }
]
},
"storage": {
"buckets": [
{ "name": "uploads", "type": "protected", "accessRules": {} }
]
}
}
}tokenPrivate— used for privileged operations (deploy, configs, settings, rules).tokenPublic— safe to embed in client applications.mongo.databases[].accessRulesandstorage.buckets[].accessRules— declarative access control, deployed withvoid-cli mongo rules deploy/void-cli storage rules deploy. See Access rules for the full schema.
Global admin config
Admin authentication is stored separately in ~/.void.json (managed by
void-cli admin login / logout). It holds your admin secret and is not tied to a
project directory.
Environment variables
The CLI talks to the Void platform at https://void.void-walkers.com by default. You can
override the endpoints (e.g. for staging or self-hosted installs):
| Variable | Default | Purpose |
| --------------- | --------------------------------- | ------------------------------ |
| BASE_URL | https://void.void-walkers.com | Base URL for all services |
| BASE_URL_API | value of BASE_URL | API endpoint |
| BASE_URL_CDN | value of BASE_URL | CDN / file upload endpoint |
Companion libraries
The CLI is the deploy/management tool; your project's runtime code uses two npm packages. Together they cover both sides of a Void project: functions you deploy, and a client SDK you call from browsers, functions or other backends.
| Package | Where it runs | Purpose |
| --------------------------- | ------------------------------------- | ----------------------------------------------------------------- |
| @voidwalkers/void-functions | inside functions/ (deployed code) | Register serverless functions and read project configs |
| @voidwalkers/void-client | browsers / functions / any backend | SDK for auth, MongoDB, object storage, realtime and function calls |
@voidwalkers/void-functions
Used inside the functions/ package (the one the CLI scaffolds and deploys). It exposes
the registration entry points the Void runtime calls into:
import {registerHttpFunction, registerCallFunction, getConfigValue} from '@voidwalkers/void-functions';
// HTTP function — invoked over HTTP, gets Express-like (req, res)
registerHttpFunction('hello-world', (req, res) => {
res.send('Hello World!');
});
// Call function — invoked via the client SDK's voidClient.function(name)(data)
registerCallFunction('echo', (data, context) => {
// context.projectUser === {id} of the authenticated user (or null)
return {context, data};
});
// Read a project config value set with `void-cli config set <key> <value>`
const flag = await getConfigValue('featureFlag', false); // second arg = defaultThe optional third options argument controls how a function is exposed (this is where the
exported enums are used):
import {registerCallFunction, FunctionSystemRole, FunctionTriggerSubject, FunctionTriggerEvent}
from '@voidwalkers/void-functions';
registerCallFunction('auth', handler, {systemRole: FunctionSystemRole.UserAuth}); // user-auth handler
registerCallFunction('on-write', handler, { // event-triggered
trigger: {subject: FunctionTriggerSubject.MongoDocument, event: FunctionTriggerEvent.Write}
});
registerHttpFunction('internal', handler, {isPrivate: true}); // not client-callableSetting any of isPrivate, systemRole or trigger makes the function private (deployed
but not directly callable by clients). Call vs Http is chosen by which register function you
call — not via options.
| Export | Purpose |
| ------------------------------------------- | ------------------------------------------------------------------- |
| registerHttpFunction(name, fn, options?) | Register an HTTP-triggered function (req, res) => … |
| registerCallFunction(name, fn, options?) | Register a callable function (data, context) => result |
| getConfigValue(key, defaultValue?) | Read a project config (throws if missing and no default given) |
| FunctionSystemRole | Enum for options.systemRole — e.g. UserAuth |
| FunctionTriggerEvent / FunctionTriggerSubject | Enums for options.trigger (create/update/delete/write/schedule; auth_user/mongo_document/schedule) |
| ProjectUserCredentialsType | Enum — Custom / Email |
See the void-functions README for
the full options reference.
Deploy registered functions by name (or all of them):
void-cli deploy -F # all functions
void-cli deploy -F echo hello-world@voidwalkers/void-client
The client SDK. Construct it with the project's public token (safe to ship to browsers);
get the snippet via void-cli credentials print client.
import {VoidClient} from '@voidwalkers/void-client';
const voidClient = new VoidClient({
project: {slug: 'my-project', tokenPublic: '<public token>'}
});
// Auth
await voidClient.auth.signInCustom({email: '[email protected]', password: 'secret'});
const user = await voidClient.auth.getCurrentUser(); // {id: 'UUID'}
// MongoDB (queries are subject to the database access rules below)
await voidClient.mongo.collection('items').insertOne({type: 'box', name: 'empty box'});
const {data} = await voidClient.mongo.collection('items').find({type: 'box'}, {limit: 50});
voidClient.mongo.collection('items').watch({type: 'box'}, ({type, data}) => { /* realtime */ });
// Object storage (subject to bucket access rules below)
await voidClient.storage.bucket('photos').createFile(blobOrStream, 'kitten.jpg', ['kittens']);
await voidClient.storage.bucket('photos').listFiles({prefix: ['kittens'], limit: 100});
// Call a registered call-function
const result = await voidClient.function('echo')({hello: 'world'});Surface: voidClient.auth, voidClient.mongo (.collection(name) / .db(name).collection(name)),
voidClient.storage.bucket(name), voidClient.function(name). See the
void-client README for the full method list and option shapes.
Access from the client is gated by the access rules you deploy with the CLI. Read Access rules before writing rules — the client cannot do anything the rules don't permit.
Commands
Run void-cli <command> --help for detailed, up-to-date usage of any command.
init
Initialize a Void project in the current directory. Prompts for a project slug, private
token and public token, then creates void.json and scaffolds the selected components.
void-cli init
void-cli init -c functions websites # only enable specific components| Option | Description |
| ------------------------------- | ---------------------------------------------------------------- |
| -c, --components [components…] | Components to enable: functions, websites, mongo (all by default) |
Fails if void.json already exists.
enable
Enable one or more components in an already-initialized project. Components that are already enabled are skipped.
void-cli enable functions
void-cli enable mongo websitesAccepted components: functions, websites, mongo.
deploy
Deploy project parts to the platform. With no flags, deploys everything (functions and
websites). Deploying functions runs npm run lint and npm run test in the functions/
directory first (when those scripts exist).
void-cli deploy # deploy all functions and websites
void-cli deploy -F # deploy all functions
void-cli deploy -F hello-world api # deploy specific functions
void-cli deploy -W # deploy all websites
void-cli deploy -W default # deploy a specific website| Option | Description |
| ------------------------------- | -------------------------------------------- |
| -F, --functions [functions…] | Deploy functions (optionally by name) |
| -W, --websites [websites…] | Deploy websites (optionally by name) |
config
Manage project key/value configs (read by your functions at runtime).
void-cli config set <key> [value]
void-cli config set featureFlag true -t boolean
void-cli config set maxItems 50 -t number
void-cli config set apiKeys '["a","b"]' -t array
void-cli config set someKey # omit value to set null| Option | Description |
| ------------------- | --------------------------------------------------------------------- |
| -t, --type <type> | Value type: boolean, number, string, object, array |
settings
Manage built-in project settings (e.g. authentication providers).
void-cli settings list # show all settings, types and defaults
void-cli settings set auth.email.isEnabled true
void-cli settings set auth.email.providers.resend.apiKey "re_xxx"| Subcommand | Description |
| -------------------- | -------------------------------------------- |
| list | List available settings with types/defaults |
| set <key> [value] | Set a setting value (omit value to set null) |
Setting keys are dot-paths, e.g. auth.email.provider. Values are validated against the
type declared for each setting.
credentials
Print project credentials from void.json in different formats.
void-cli credentials print # client format (default)
void-cli credentials print client # slug + public token (safe for clients)
void-cli credentials print server # slug + private token
void-cli credentials print raw # entire void.jsonmongo
Manage the project's MongoDB instance.
# Databases
void-cli mongo database create <name>
void-cli mongo database drop <name>
# Connection credentials
void-cli mongo credentials print # internal connection string
void-cli mongo credentials print internal -d default
void-cli mongo credentials print internal --pretty # JS snippet
void-cli mongo credentials print external # (not implemented yet)
# Access rules
void-cli mongo rules deploy # deploy rules for "default" database
void-cli mongo rules deploy -d analytics| Subcommand | Options | Description |
| -------------------------------- | --------------------------------------------------- | -------------------------------------------- |
| database create <name> | | Create a MongoDB database |
| database drop <name> | | Drop a MongoDB database |
| credentials print [variant] | -d, --database <name>, -p, --pretty | Print connection string (internal/external) |
| rules deploy | -d, --database <name> | Deploy access rules from void.json |
Access rules are read from project.mongo.databases[].accessRules in void.json and pushed
to the platform. See Access rules for the full schema, the expression
language and the available context variables.
storage
Manage object storage buckets and files.
# Buckets
void-cli storage buckets create <name>
void-cli storage buckets create uploads -t public
# Upload files
void-cli storage buckets upload <bucket> <file>
void-cli storage buckets upload uploads ./logo.png -n logo.png -p images
# Access rules
void-cli storage rules deploy # all buckets
void-cli storage rules deploy -b uploads avatars # specific buckets| Subcommand | Options | Description |
| ----------------------------------- | ----------------------------------------------- | ------------------------------------ |
| buckets create <name> | -t, --type <type> (private/protected/public, default protected) | Create a bucket |
| buckets upload <bucket> <file> | -n, --name <name>, -p, --prefix <prefix> | Upload a local file to a bucket |
| rules deploy | -b, --buckets <buckets…> | Deploy bucket access rules |
Access rules are read from project.storage.buckets[].accessRules in void.json. See
Access rules for the full schema and the available context variables.
websites
Add websites and manage their custom domains. Custom domains are managed only via these
commands — deploy never touches them.
# Add a new website
void-cli websites add <slug>
void-cli websites add shop
# Custom domains
void-cli websites domains set <website> <subdomain>.<root>
void-cli websites domains set default shop.void-walkers.com
void-cli websites domains remove <website>
void-cli websites domains list| Subcommand | Description |
| ---------------------------------- | ----------------------------------------------------------- |
| add <slug> | Add a new website: scaffold websites/<slug>/dist/index.html and register it in void.json |
| domains set <website> <domain> | Set a custom domain (<subdomain>.<root>) for a website |
| domains remove <website> | Remove the custom domain from a website |
| domains list | List each website's default and custom domain (void.json) |
<slug>is used as a Kubernetes pod name during website initialization, so it must be K8s-safe: lowercase alphanumeric characters or-, starting and ending with an alphanumeric character, no dots, and at most 120 characters.The website is appended to
websites.itemsinvoid.jsonwithpublic: "./dist". If thewebsites/<slug>/distdirectory already exists it is left untouched.Requires the
websitescomponent to be enabled (void-cli enable websitesorvoid-cli init).The
<domain>argument is split on the first dot: the first label is thesubdomain, the remainder is theroot. It is stored invoid.jsonascustomDomain: { root, subdomain }.One custom subdomain per website (enforced by the API).
Allowed roots:
void-walkers.com,ribogdanova.com.The website must be deployed first before a custom domain can be set; otherwise the API returns
404and the CLI hints you to runvoid-cli deploy -W <website>.listreads onlyvoid.json(no API call) and shows each website's default domain (void-<slug>[-<name>].void-walkers.com) plus its custom domain when set.
admin
Administrative tasks. Requires an admin secret stored via admin login (kept in
~/.void.json).
# Authentication
void-cli admin login
void-cli admin logout
# Users
void-cli admin users create [email]
void-cli admin users list
# Projects
void-cli admin projects create <slug> -u <userId>
void-cli admin projects create my-app -u 123 --tokens # also create default tokens
void-cli admin projects create my-app -u 123 --private-token "My token"
void-cli admin projects list -u <userId>| Subcommand | Options | Description |
| ------------------------------ | --------------------------------------------------------- | ------------------------------------ |
| login | | Store the admin secret |
| logout | | Remove the stored admin secret |
| users create [email] | | Create a user (prompts for email if omitted) |
| users list | | List all users |
| projects create <slug> | -u, --user <id> (required), --private-token [name], --public-token [name], -t, --tokens | Create a project for a user |
| projects list | -u, --user <id> (required) | List a user's projects |
Access rules
Access rules are declarative authorization for MongoDB and object storage. They are
authored in void.json, then pushed to the platform with void-cli mongo rules deploy /
void-cli storage rules deploy. Every query a client makes through @voidwalkers/void-client
is evaluated against them server-side — there is no way for a client to bypass them.
The CLI translates the friendly void.json form into the platform's wire format on deploy
(see src/lib/rules.js, src/lib/components/mongo.js, src/lib/components/storage.js), so
you only ever write the form documented here.
The rule expression language
A rule is a small boolean expression. The CLI accepts a MongoDB-like syntax and converts it
before deploying:
| Form | Meaning |
| --------------------------- | ------------------------------------------------------------------- |
| {"$and": [r1, r2, …]} | all sub-rules must be true (1+ items) |
| {"$or": [r1, r2, …]} | at least one sub-rule true (1+ items) |
| {"$eq": [a, b]} | a equals b (exactly 2 items) |
| "$$name.path.0" | a variable reference — $$ + dotted path; numeric segments index arrays |
| any literal (true, 42, "x") | a constant value |
You may also write the already-converted raw form directly, e.g. a constant allow/deny:
{"rule": {"type": "value", "value": true}} // always allow
{"rule": {"type": "value", "value": false}} // always denyImportant: this
ruleengine only understands$and/$or/$eq/$$var/ literals (seesrc/lib/rules.js). It is not full MongoDB query/aggregation. The richer MongoDB aggregation syntax is only available insidepipeline,lookupandredactstages (described below), which actually run in MongoDB.
Context variables
The variables available to a rule depend on the operation:
| Variable | Available in | Shape |
| -------------------- | --------------------------------------------- | ------------------------------------------------------------ |
| $$voidProjectUser | all operations | {id} of the authenticated user, or null if anonymous |
| $$voidDoc | mongo create | the document being inserted |
| $$voidResult | mongo create, all bucket ops | the array returned by the operation's pipeline (or null) |
| $$voidUpdate | mongo update (inside redact) | the update doc with $ stripped, e.g. {set: {…}, unset: {…}} |
| $$voidFile | all bucket operations | {name, prefix} — prefix is the path-segment array |
MongoDB rules
void.json shape — keyed by collection name, then by operation:
"mongo": {
"databases": [
{
"name": "default",
"accessRules": {
"<collectionName>": {
"create": { "collection": "...", "pipeline": [ … ], "rule": { … } },
"read": { "lookup": [ … ], "redact": { … } },
"update": { "lookup": [ … ], "redact": { … } },
"delete": { "lookup": [ … ], "redact": { … } }
}
}
}
]
}Operations map to the client methods on voidClient.mongo.collection(name):
| Operation | Triggered by | How the rule is enforced |
| --------- | ----------------------------------------- | ---------------------------------------------------------------------------------------- |
| create | insertOne / insertMany | optional pipeline runs against collection → $$voidResult; then rule must be true |
| read | find / findOne / watch | redact is a MongoDB $redact stage deciding $$KEEP / $$PRUNE per document |
| update | findOneAndUpdate / updateOne / … | same $redact mechanism; $$voidUpdate exposes the requested changes |
| delete | deleteOne / deleteMany | same $redact mechanism |
Per-operation fields:
createcollection(optional) — another collection to look something up in first.pipeline(optional) — a MongoDB aggregation pipeline run againstcollection; it can reference$$voidDocand$$voidProjectUser. Its output becomes$$voidResult.rule— the boolean expression; insertion is allowed only if it evaluates truthy. Can use$$voidDoc,$$voidProjectUser,$$voidResult.
read/update/deleteredact— a MongoDB$redactexpression returning$$KEEP/$$PRUNE/$$DESCEND. Runs inside MongoDB, so it uses full aggregation syntax and can reference document fields ($field),$$voidProjectUser, and (forupdate)$$voidUpdate.lookup(optional) — an array of$lookupstage bodies injected before the redact stage (joined fields are projected back out afterwards).
A collection with no rule for an operation rejects that operation by default — start by writing rules for every operation a client needs.
Object storage rules
void.json shape — keyed directly by operation:
"storage": {
"buckets": [
{
"name": "user-files",
"type": "protected",
"accessRules": {
"create": { "rule": { … } },
"read": { "collection": "...", "database": "...", "pipeline": [ … ], "rule": { … } },
"update": { "rule": { … } },
"delete": { "rule": { … } },
"list": { "rule": { … } }
}
}
]
}Bucket types (-t on storage buckets create):
| Type | Behaviour |
| ----------- | --------------------------------------------------------------------------------- |
| public | files served publicly by URL; no per-request access rules |
| protected | every operation is gated by the access rules below (default) |
| private | no direct client access — reachable only from server-side / privileged code |
Operations: create, read, update, delete, list (mapping to
voidClient.storage.bucket(name) methods). Per-operation fields:
rule— boolean expression; the operation is allowed only if it evaluates truthy. Can use$$voidFile,$$voidProjectUser, and$$voidResult.collection+pipeline(optional) — run a MongoDB aggregation (againstdatabase, defaultdefault) before evaluatingrule; the pipeline can reference$$voidFileand$$voidProjectUser, and its output becomes$$voidResult.database(optional) — which MongoDB database the lookupcollectionlives in; defaults todefaultwhencollectionor a non-emptypipelineis present.
Worked example
A user-files bucket where each user may only read files under a prefix equal to their own id,
and an items collection where users only see and mutate their own rows:
{
"project": {
"mongo": {
"databases": [
{
"name": "default",
"accessRules": {
"items": {
"create": {
"collection": "users",
"pipeline": [
{"$match": {"$expr": {"$eq": ["$voidId", "$$voidDoc.userId"]}}}
],
"rule": {
"$and": [
{"$eq": ["$$voidDoc.userId", "$$voidProjectUser.id"]},
{"$eq": ["$$voidResult.length", 1]}
]
}
},
"read": {
"redact": {
"$cond": {
"if": {"$eq": ["$$voidProjectUser.id", "$userId"]},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}
}
}
}
}
]
},
"storage": {
"buckets": [
{
"name": "user-files",
"type": "protected",
"accessRules": {
"create": {"rule": {"type": "value", "value": true}},
"read": {
"collection": "users",
"pipeline": [
{"$match": {"$expr": {"$eq": ["$voidId", "$$voidProjectUser.id"]}}}
],
"rule": {
"$and": [
{"$eq": ["$$voidResult.length", 1]},
{"$eq": ["$$voidFile.prefix.length", 1]},
{"$eq": ["$$voidFile.prefix.0", "$$voidProjectUser.id"]}
]
}
},
"list": {"rule": {"type": "value", "value": true}}
}
}
]
}
}
}Deploy after editing:
void-cli mongo rules deploy # database "default"
void-cli storage rules deploy -b user-files # one or more buckets (all if omitted)Project layout
After init, a typical project looks like:
.
├── void.json # project config (gitignored — contains tokens)
├── functions/ # serverless functions package (its own package.json)
│ └── src/index.js
└── websites/
└── default/
└── dist/
└── index.html- functions/ is a standalone Node package using
@voidwalkers/void-functions. The CLI runsnpm installthere on init, andnpm run lint/npm run teston deploy. - websites/ holds one directory per website; the
publicsub-path (default./dist) is what gets deployed.
Development
npm install
npm run start # run the CLI from source (node ./src/app.js)
npm run lint # eslint
npm test # mochaLicense
UNLICENSED — © Andrey Bogdanov. Internal Void Walkers project.
