Fasty is a Blazing Fast CMS built on top of openresty & arangoDB


You'll need the following tools installed on your computer

  • NodeJS
  • NPM or Yarn
  • docker & docker-compose
  • foxxy NPM module installed globally
npm install foxxy -g
npm install foxx-cli -g

# optional for using your own code editor
npm install fasty-cli -g 

You'll have to update your ~/.foxxrc file with 


Get Started

Fasty is based on Openresty, Lua & ArangoDB. You'll have the full stack installed via Docker. Pull the repository and inside it run :

docker-compose build
docker-compose run --rm cms moonc **/*.moon
docker-compose up cms

You have first to create a foxxy/app/js/config.js file (you can copy content from foxxy/app/js/config.js.sample)

The application should run on http://demo.localhost:8080 and http://demo.localhost:8080/static/admin . The database is running on http://localhost:8529 (login : root, password: password)

You'll need to create first a database called db_demo and run within the foxxy folder

foxxy upgrade settings --server foxxy --database db_demo
foxxy upgrade --server foxxy --database db_demo

You should be able now to access the admin URL with the following credentials : 

[email protected] :: 977cebdd

Don't forget to change this default account via the /static/admin page !

Folders rights

You'll also need to update ownership of <app_folder>/install_service & <app_folder>/scripts to be writable by your docker process. If you are unsure use the same user/group from <app_folder>/fastcgi_temp folder. Without that, you won't be able to install API or install scripts (nodejs)

Create certificates for google cloud storage (optional)

Fasty allows you to store your assets in the google cloud. For that you just need to create a folder called `/certs` in the root of your application. For your multi-tenant apps you can define one file per sub domain. 

  -- cms.json
  -- app1.json
  -- default.json

it will fallback to `default.json` if no match. Here a sample of the json data :

  "type": "service_account",
  "project_id": "<your_project_id>",
  "private_key_id": "1234567890abcdef",
  "private_key": "-----BEGIN PRIVATE KEY-----\n ... <your_private_key>\n-----END PRIVATE KEY-----\n",
  "client_email": "client_email",
  "client_id": "client_id",
  "auth_uri": "",
  "token_uri": "",
  "auth_provider_x509_cert_url": "",
  "client_x509_cert_url": "<link_to_your_file>"


Layouts allow you to define the global template of your application. You can have multiple layouts. Each pages can be linked to any layout.

In this basic sample we can see interesting things :

css & css_vendors are CSS content defined in the layout
js & js_vendors are JS content defined in the layout

@headers will be replaced by generated headers

will display the content of your page

you can also notice the en to display the current language & some {{ partials | ... }} to display shared parts

css_vendors :

{{ external | }}
{{ external | }}


{{ external | }}
{{ external | }}
{{ external | }}

You can define as many external resources you need.


Pages are the pages of your website. You can create as many pages you want.

Notice that the slug must be unique across all pages. The default url schema is : /:lang/:all/:slug(/*)

:lang is the current lang (e.g. en, fr, etc.)
:all can be anything you want (e.g. -, my, anything, useful, etc.)
is your page's slug (e.g. home, page, etc)

Any additional path element will be stored in the splat params.
Sample : /en/my/home/page/10

splat params will looks like { page: 10 }



Display any page content via it's slug

{{ page | <slug_or_field> (| <dataset>) }}

{{ page | my_famous_page }} << Display page content of the my_famous_page page

{{ page | slug | posts }} << This one will display the html field from your posts dataset using the slug defined in the URL


Display partials via it's slug

{{ partial | <partial_name> (| <dataset> | <options>) }}

But a partial can have a dataset (arango, rest or use_params) and you can also use do_not_eval as dataset if you just want to include etlua file without evaluating it directly.

{{ partial | demo | arango | aql#FOR user IN users RETURN user }}

{{ partial | pagination | use_params | current_page#1#pages#10 }}

[string "etlua"]:5: attempt to index field '_params' (a nil value)


Display the current lang

{{ lang }}


Display an element set in the settings/home section (json) 

{{ settings | <mailgun_api> }} will fetch data from json data


DIsplay a single page application

{{ spa | <my-app> }}


Mount a riot tag.

{{ riot | tag1 }} Load the widget but don't mount it

{{ riot | tag1 | mount }} Load and mount the widget

{{ riot | tag1#tag2 }} Load tag1 & tag2 (reduce http calls)


Display a translated text. If the key don't exist it will create an entry in the traductions list.

{{ tr | welcome }}

It will display a missing message if the translation is not existing:  welcome


Display a value from the splat params (see pages section)

{{ splat | <splat_key> }}


Display a helper (AQL + partial). You can now add options

{{ helper | <helper_name> (| key1#value1#key2#value2...) }}


If you need to execute a request just use

{{ aql | <your_aql_request> }}


You can define a request per page to define headers dynamically. 

Let say we have this AQL request defined : 

FOR doc IN datasets
  FILTER doc.type == "articles" AND doc.slug == @slug
  RETURN { title: doc.title }

You could use then this shortcut in any header fields to get the content. You can also define a default value if og_data is not provided or empty.

{{ og_data | title | <default> }}


To display a content from a dataset within the page partial.

{{ html | <key> | <field> }}

You can also use the og:aql request to get the json object to be transformed within the page partial

{{ html | <key_defined_in_og:aql> }}


If you need to load an external JSON resource and extract / display a specific key you need to use :

{{ json | url | key }}

The key can be a nested one (e.g owner.login)


Display a specific key from a dataset

{{ dataset | key | field | <args> }}

Can be useful for loading JS or CSS content from a dataset record

e.g. :
{{ dataset | slug=myscript | js }}

The second one will return the URL only as js file (using the right content-type)


Components are JS components (RiotJS for now)


  <!-- layout -->
  <h3>{ opts.title }</h3>

    <li each={ item, i in items }>{ item }</li>

  <form onsubmit={ add }>
    <input ref="input">
    <button>Add #{ items.length + 1 }</button>

  <!-- style -->
    h3 {
      font-size: 14px;

  <!-- logic -->
    this.items = []

    add(e) {
      var input = this.refs.input
      input.value = ''



Partials are .etlua files ... It can be a simple raw HTML content or can embed some logic via LUA code.

You can call a partial with a specific dataset (AQL request, or external JSON data)

<div class="field is-grouped is-grouped-multiline">
  <% for k, item in pairs(dataset.results) do %>
  <div class="control">
    <div class="tags has-addons">
      <span class="tag"><%=[lang] %></span>
      <span class="tag is-primary"><%= item.state[lang] %></span>
  <% end %>

With the following AQL request

FOR doc IN datasets
FILTER doc.type == 'features'
SORT doc.order ASC

You can define a helper called features to load this partial & AQL and then call this helper with {{ helper | features }}

Single Page Applications

Create as many SPA you want. An application has on HTML tag to load components and one JS tag to set the router
{{ riot | account#plans#tchins#offer_tchins#tchins_history#friends#pending_invitations#pending_approvals#blocked }}

<div id="app"></div>
document.addEventListener('DOMContentLoaded', function() {
  route('/', function(name) { riot.mount('div#app', 'account') })
  route('/plans', function(name) { riot.mount('div#app', 'plans') })
  route('/tchins', function(name) { riot.mount('div#app', 'tchins') })
  route('/offer_tchins', function(name) { riot.mount('div#app', 'offer_tchins') })
  route('/tchins_history', function(name) { riot.mount('div#app', 'tchins_history') })
  route('/friends', function(name) { riot.mount('div#app', 'friends') })
  route('/pending_invitations', function(name) { riot.mount('div#app', 'pending_invitations') })
  route('/pending_approvals', function(name) { riot.mount('div#app', 'pending_approvals') })
  route('/blocked', function(name) { riot.mount('div#app', 'blocked') })


Store your AQL requests and then use them within Helpers

Datatypes & Datasets

Datatypes allow you to create dynamic forms for the content editors.

  "model": [
    { "r": true, "c": "1-1", "n": "name", "t": "string", "j": "joi.string().required()", "l": "Name", "tr": true },
    { "r": true, "c": "1-1", "n": "state", "t": "string", "j": "joi.string().required()", "l": "State", "tr": true }
  "columns": [ { "name": "name", "tr": true } ],
  "sortable": true

Once a dataset defined; a new dataype will appear and you'll be able to create & manage your data


Choose a partial, a request ... Then you can call it anywhere you want using the helper shortcut


Redirections allow you to define a specific route for a specific helper

API Builder

The API Builder allow you to write your Foxx service directly via Fasty. You can also deploy it with one click !

Server side scripts

You can edit your Nodejs server side scripts and restart them directly from Fasty. You'll have first to set the docker-compose file :
    context: .
    dockerfile: Dockerfile_node
  command: /bin/bash -c "yarn && yarn start"
  restart: always
    - 8000:8000
    - ./scripts/your_folder/chatroom/:/workspace
    - arangodb:arangodb