Goexplore

- by Goaddon

Website as a Service - launch a website in minutes, bootstrap it with other addons and start building!

goexplore@goaddon.com
Supported in

Setting up a website on secure and scalable server infrastructure is time consuming. Further securing the website with SSL and user authentication takes time as well. Yet it is not particularly creative work. Everybody goes through a similar process and end up with a similar result.

So why do it yourself? Goexplore is a website, served from your own domain, that users can sign up to. On top of it you can build web pages with the features that makes your project unique and interesting.


WIKI

It requires very little on your part to get started.

Choose the URL of your Goexplore enabled website. Say you own example.com , you could e.g. choose app.example.com or simply www.example.com .

Visit your DNS manager and create a records that points the chosen URL to our servers, as explained in the DNS section.

Finally, visit the URL and checkout your new website.

Collection hierarchy

Users who sign up through your website must belong to some kind of parent document. We are assuming that you let users belong to companies unless you change it to accounts, organizations or something else in the Collection hierarchy section.

Please make this choice before users starts to sign up. During user registration we will create a parent document in the chosen collection, and if you subsequently change it you will have to create new documents based on the old collection.

When you have decided on a parent collection, you need to choose a unique parent identifier for documents in the collection. You are advised to create a MongoDB index that enforces uniqueness on the database level. The index could look as follows:

[
  {
    "key": {
      "website": 1
    },
    "options": {
      "unique": true
    }
  }
]
Choosing languages

Your website can respond to multiple languages. The language is determined in the requested URL. If your website can be accessed at www.example.com and the user visits www.example.com/en/some_path the website will respond with the language en. Visit the Languages section to choose available languages.

Managing your website

Create a superuser for yourself in order to access the Superuser section of your website. From here you can create custom pages and follow users who has signed up on your website.

Routing for your custom pages

Before exploring how to add content to the website you should familiarize yourself with our routing principles, inspired by Ruby on Rails . The path in a URL refers to a controller and an action.

Within a controller different actions exists. For example, consider a controller called tasks:

  • A GET request for /tasks/new goes to the new action, so /tasks/new .
  • A GET request for /tasks/:id goes to the show action, so e.g. /tasks/5d0234903ab42fd9483a4807 .
  • A GET request for /tasks/:id/:action goes to a custom action, so e.g. /tasks/5d0234903ab42fd9483a4807/refund .
  • A GET request for /tasks goes to the index action, so /tasks .
  • A POST request for /tasks goes to the create action.
  • A PATCH or PUT request for /tasks/:id goes to the update action.
  • A DELETE request for /tasks/:id goes to the destroy action.
Managing the menu

The website has a main menu and sub menues.

Once signed in as a superuser, visit SUPERUSER MENU ENTRIES to create a new menu entry. These fields needs to be filled:

Name

The name determines where the menu entry belongs. A menu entry with the name tasks will be placed in the main menu. Another entry with the name tasks__flagged_for_review will be placed as its sub menu entry.

Title

The title of the menu entry should be chosen for each language. If you only support the en language it could be:

{
  "en": "Tasks"
}
Paths

Consider this hash:

[
  {
    "controller": "tasks",
    "action": "index"
  },
  {
    "controller": "tasks"
  }
]

The first item in the hash determines what path the menu entry links to. In this case it would be the index action in the tasks controller, so /tasks .

If any of the items in the hash are matched by the visited path the menu will be shown as active. In this example whenever the path refers to an action in the tasks controller. It could be the index action /tasks or the show action /tasks/5d0234903ab42fd9483a4807 .

Default menu entries

In addition to your own menu entries there is an ACCOUNT menu entry where a user can switch between accounts and manage his credentials. A menu entry called account__billing or account__delete will be added as a sub menu entry.

Superusers will additionally see a SUPERUSER menu entry. A menu entry e.g. called superuser__statistics will be added as a sub menu entry.

Creating pages

You need to create pages that corresponds to the paths linked to by the menu entries. For that you should visit SUPERUSER PAGES. These fields needs to be filled:

Name

The name of the page defines what controller and action it responds to. Building on the previously descibed menu entry it could be tasks__index, and thereby respond to the index action in the tasks controller.

HTML

The HTML of the page is where you put the content. You can use Liquid and access these variables:

account

The account of the authorized user. The account collection class depends on your database structure, as described in the Collection hierarchy section. If you don't change our defaults it could be:

{
  "_id": "5d0120e03ab42fc93f610524",
  "website": "example.com",
  "authorized_user_ids": [
    "5d0120e03ab42fc93f610525"
  ]
}

project

The project consists of a few informations, for example secrets, which you can maintain in the Secrets section , and addon_data, which can be used by other addons you are subscribed to through Goaddon.

An example:

{
  "_id": "5fd34cb83ab42f1267d1760a",
  "name": "My project",
  "zone": "eu",
  "url": "app.example.com",
  "app_id": "5bb227d283c3360abe01e036-5fd34cb83ab42f1267d1760a",
  "secrets": {
    "password": "foo",
    "api_token": "bar"
  },
  "addon_data": {
    "addon_5d0133a83ab42f187d137e4e": {
      "url": "example.com"
    }
  }
}

user

The authorized user, for example:

{
  "_id": "5d0120e03ab42fc93f610525",
  "email": "example@example.com",
  "encrypted_password": "$2a$11$U4nYfzff8Hbbd1RaA7gHVOU2M46ILg7kzIfNp/y.RqMglBof7rwdS",
  "sign_in_count": 1,
  "failed_attempts": 0,
  "company_id": "5d0120e03ab42fc93f610524",
  "superuser": false,
  "superuser_level": nil,
  "confirmation_token": "Z2mu_d4ghEu_MNBcsoXP",
  "confirmation_sent_at": "2019-06-12 15:57:35 UTC",
  "confirmed_at": "2019-06-12 15:57:35 UTC",
  "reset_password_sent_at": "2019-06-12 15:57:35 UTC",
  "reset_password_token": "FM2yuQsEt4E7SZxYgDPA",
  "current_sign_in_at": "2019-06-12 15:57:35 UTC",
  "current_sign_in_ip": "127.0.0.1",
  "last_sign_in_at": "2019-06-12 15:57:35 UTC",
  "last_sign_in_ip": "127.0.0.1"
}

params

Parameters from the URL or request body, for example:

{
  "id": "5d0234903ab42fd9483a4807",
  "foo": "bar"
}

session

Basic informations about the session, for example:

{
  "zone": "eu",
  "protocol": "https://",
  "url": "app.example.com",
  "repository_id": "610bd7333ab42f18275b7cae",
  "controller": "tasks",
  "action": "index",
  "page": "tasks__index",
  "format": "text/html",
  "ip": "000.000.000.00"
}

The format could be e.g. text/html, application/json or text/javascript.

image_names

The image names that are available to the project, for example:

[
  "logo.png",
  "red_flowers.jpg"
]

The array will be empty if you do not indicate that you need to use images, as described in the Images section.

headers

Currently this includes only one information, which is the authorization header, if any:

{
  "authorization": "Basic YWRhZnczcjItM2QyNC00M2QwLXI4NGYtNWc5OHRoOWVqYmY2"
}
Locales

You can keep your HTML code clean by putting all text content into the locales hash, like this:

{
  "en": {
    "title": "Your tasks - example.com",
    "headline": "Tasks"
  }
}

and then reference them like this in your HTML:

<h5 class="mb-4">{{ l.headline }}</h5>

The title key will be used as title in the browser tab.

Queries

You can interact with your Goaddon database. View database demo website .

The following operators are available:

db.find()

You can query your Goaddon database by defining a query hash like this:

{
  "tasks": {
    "klass": "tasks",
    "query": {
      "company_id": "{{ company_id }}"
    },
    "pagination": {
      "page": 0,
      "per_page": 20
    },
    "projection": {
      "_id": 1,
      "order_number": 1
    },
    "sort": {
      "_id": -1
    }
  }
}

and referencing it like this in your HTML code:

{% assign tasks = "tasks" | db_find: company_id: account._id, page: params.page, per_page: params.per_page %}

This will return e.g.:

{
  "results": [
    {
      "_id": "5d0234903ab42fd9483a4807",
      "order_number": "123"
    }
  ],
  "results_count": 1,
  "total_results_count": 1,
  "pages_count": 1,
  "page": 0,
  "per_page": 20,
  "error": nil,
  "query": {
    "company_id": "5fbe6b233ab42f1267d17606"
  }
}

Notice how we replaced the {{ company_id }} tag with the company_id argument. It is possible to insert any number of tags into any hash value, but hash keys are static. If you need to determine the hash key dynamically, you can pass the entire hash as argument.

You can also do a count operation like this:

{% assign tasks = "tasks" | db_count: company_id: account._id %}

which wil return e.g.:

{
  "results_count": 1
}

Pagination of your query will only happen if your query hash has a pagination key, as in the example above. If you don't pass page and per_page arguments we will use the ones that you have chosen as default.

db.insert()

You can insert a document into your Goaddon database by defining it like this:

{
  "create_task": {
    "klass": "tasks",
    "document": {
      "company_id": "{{ company_id }}",
      "name": "{{ name }}"
    }
  }
}

and referencing it like this in your HTML code:

{% assign create_task = "create_task" | db_insert_one: company_id: account._id, name: "Work all day" %}

This will return e.g.:

{
  "created_count": 1,
  "document": {
    "_id": "5d0234903ab42fd9483a4807",
    "company_id": "5fbe6b233ab42f1267d17606",
    "name": "Work all day"
  }
}

You can insert many documents into your Goaddon database with a query like this:

{
  "create_tasks": {
    "klass": "tasks",
    "documents": "{{ tasks }}"
  }
}

where tasks is contructed like this:

{% liquid
  assign tasks = "" | split: " "
  assign names = "Work all day,Work all night" | split: ","

  for name in names

    assign task  = nil | new_hash: name: name, company_id: account._id
    assign tasks = tasks | push_to_array: task

  endfor

  assign create_tasks = "create_tasks" | db_insert_many: tasks
%}

This will return e.g.:

{
  "created_count": 2,
  "documents": [
    {
      "_id": "5d0234903ab42fd9483a4807",
      "company_id": "5fbe6b233ab42f1267d17606",
      "name": "Work all day"
    },
    {
      "_id": "62b2c41983c3360b268a23c9",
      "company_id": "5fbe6b233ab42f1267d17606",
      "name": "Work all night"
    }
  ]
}

db.update()

You can update a document in your Goaddon database by defining a hash like this:

{
  "update_task": {
    "klass": "tasks",
    "query": {
      "company_id": "{{ company_id }}",
      "_id": "{{ _id }}"
    },
    "document": {
      "$set": {
        "name": "{{ name }}"
      }
    }
  }
}

and referencing it like this in your HTML code:

{% assign update_task = "update_task" | db_update_one: company_id: account._id, _id: params.id, name: "Work all day and night" %}

This will return e.g.:

{
  "matched_count": 1,
  "updated_count": 1,
  "query": {
    "company_id": "5fbe6b233ab42f1267d17606",
    "_id": "5fbe6b373ab42f1267d17607"
  },
  "document": {
    "$set": {
      "name": "Work all day and night"
    }
  }
}

db.delete()

You can delete a document in your Goaddon database by defining a hash like this:

{
  "destroy_task": {
    "klass": "tasks",
    "query": {
      "company_id": "{{ company_id }}",
      "_id": "{{ _id }}"
    }
  }
}

and referencing it like this in your HTML code:

{% assign destroy_task = "destroy_task" | db_delete_one: company_id: account._id, _id: params.id %}

This will return e.g.:

{
  "deleted_count": 1,
  "query": {
    "company_id": "5fbe6b233ab42f1267d17606",
    "_id": "5fbe6b373ab42f1267d17607"
  }
}

If you want to delete as many documents as can be matched by your query, you can use the delete_many filter instead of delete_one:

{% assign destroy_tasks = "destroy_tasks" | db_delete_many: company_id: account._id %}
Webhooks

You can request an external URL by defining a webhook hash like this:

{
  "event_occurred": {
    "method": "post",
    "url": "https://{{ url }}/api/v1/events",
    "headers": {
      "Authorization": "{{ token }}",
      "Content-Type": "application/json"
    },
    "body": {
      "id": "{{ _id }}",
      "type": "{{ foo }}"
    }
  }
}

and then referencing it like this in your HTML code:

{% assign event_occurred = "event_occurred" | webhook: url: project.addon_data.addon_5d0133a83ab42f187d137e4e.url, token: project.secrets.token, _id: account._id, foo: "bar" %}

This will return e.g.:

{
  "code": 200,
  "body": {
    "foo": "bar"
  },
  "request_url": "https://example.com/api/v1/events",
  "request_headers": {
    "Authorization": "foo",
    "Content-Type": "application/json"
  },
  "request_body": {
    "id": "5fbe6b233ab42f1267d17606",
    "type": "bar"
  }
}

All HTTP methods are supported, so get, post, patch, put and delete.

You can not dynamically insert the webhook name in your Liquid tag. The name needs to be declared as a string, not a variable.

View webhook demo website .

Container

A page can belong to a container, as descriped in the next section.

Containers

By default, a page can only be accessed by an authenticated user. To make a page accessible to the public you need to put the page inside a container that don't require authentication. Examples of public pages could be your frontpage or your Terms of Service.

Containers works similar to pages. When a page belongs to a container it will be displayed inside the HTML of the container, where ever you add a {% yield %} tag. Sharing a container is especially usefull for pages visible for the public, because elements like your own menu and footers would be shared between multiple pages. A page hidden behind authentication will be displayed as just one of three elements (the other being a top bar and a left hand menu). However, publicly available pages will be the only thing rendered inside the body:

<html>
  <head>
    <!-- scripts and styles -->
  </head>
  <body class="d-flex flex-column">
    <!-- Your container -->
  </body>
</html>

You can create a container by visiting SUPERUSER CONTAINERS.

Snippets

If you find yourself duplicating the same lines of code on different pages or containers, you could move these lines to a snippet.

The snippet content will be placed wherever you reference it, and it will have access to the same variables as if it was a part of the page or container that referenced it.

Consider a situation where several pages wants to display a status of some kind:

<!-- Page name: tasks__index -->
<p>{{ params.foo }}</p>

<!-- Page name: tasks__show -->
<p>{{ params.foo }}</p>

Instead of duplicating content across the pages you could visit SUPERUSER SNIPPETS and create a snippet with the shared content:

<!-- Snippet name: foo_param -->
<p>{{ params.foo }}</p>

and reference the snippets name on each page.

<!-- Page name: tasks__index -->
{% include "foo_param" %}

<!-- Page name: tasks__show -->
{% include "foo_param" %}

Snippets can also be referenced by other snippets.

You can link between your own pages by using Liquid like this:

<!-- GET /en/tasks -->
<a href="{{ 'tasks__index' | path_for }}">{{ l.visit_tasks }}</a>

<!-- POST /en/tasks.js -->
<form action="{{ 'tasks__create' | path_for: format: 'js' }}" method="post" data-remote="true">
  <input type="submit" value="{{ l.create_task }}">
</form>

<!-- GET /en/tasks/5d0234903ab42fd9483a4807 -->
<a href="{{ 'tasks__show' | path_for: id: task._id }}">{{ l.show_task }}</a>

<!-- GET /en/tasks/5d0234903ab42fd9483a4807/reassign -->
<a href="{{ 'tasks__reassign' | path_for: id: task._id }}">{{ l.reassign_task }}</a>

<!-- PATCH /en/tasks/5d0234903ab42fd9483a4807.js -->
<form action="{{ 'tasks__update' | path_for: id: task._id, format: 'js' }}" method="patch" data-remote="true">
  <input type="submit" value="{{ l.update_task }}">
</form>

<!-- DELETE /en/tasks/5d0234903ab42fd9483a4807.js -->
<form action="{{ 'tasks__destroy' | path_for: id: task._id, format: 'js' }}" method="delete" data-remote="true">
  <input type="submit" value="{{ l.destroy_task }}">
</form>

<!-- POST /en/tasks.js with confirm overlay -->
<form action="{{ 'tasks__create' | path_for: format: 'js' }}" method="post" data-remote="true">
  <input type="submit" value="{{ l.create_task }}" data-title="{{ l.are_you_sure }}" data-confirm="{{ l.this_will_create_the_task }}" data-commit="{{ l.ok }}" data-cancel="{{ l.cancel }}" data-disable-with="{{ l.please_wait }}" data-commit-class="btn btn-outline-primary" data-cancel-class="text-primary">
</form>

The last exemplifies how you can insert a confirmation overlay before the form is submitted.

Adding custom content into the website head

If you want to insert custom content, for example script and style tags, into the head of the website you can do so.

You can create head content by visiting SUPERUSER HEADS. You can only create one head entry through the interface, and this will only be used if the visited page has also been created through the interface.

Your head content has access to localization, but can not query your database or in general call anything else than basic Liquid methods.

Managing scripts and styles

An alternative to adding scripts and styles into the head directly would be to maintain them in separate files.

You can create these by visiting SUPERUSER SCRIPTS and SUPERUSER STYLES. You can only create one of each through the interface, and they will only be used if the visited page has also been created through the interface.

In order to pull the scripts into the browser of a user you need to reference them like this:

<script src="{{ 'script__index' | path_for: repository_id: session.repository_id }}"></script>
<link href="{{ 'style__index' | path_for: repository_id: session.repository_id }}" rel="stylesheet" media="all">

Ideally you should fetch these resources from within your head.

You have access to localization, but can not query your database or in general call anything else than basic Liquid methods.

Adding terms to you website

Users signing up on your website would be required to accept your Terms of Service, and the website will assume that these are made available at /terms . To make that page available to the public, you need to create a terms__index page:

<p>Dear visitor, please find our terms below.</p>
<p>To use our services, you agree to...</p>

To make the page publicly available, create a container that does not require authentication. You could for example call it unauthenticated. Let's not fill it it with any HTML right now. Just place a {% yield %} tag inside it:

{% yield %}

Your Terms of Service are now available yo the public. But once users sign up they can create additional accounts for themselves. The website will ensure that they also accept your terms on behalf of such new accounts. Therefore the terms themselves would need to be available to authenticated users too. Therefore move the terms to a terms snippet:

<p>To use our services, you agree to...</p>

And then include them on your terms__index page:

<p>Dear visitor, please find our terms below.</p>
{% include "terms" %}

With this setup, you can display the content of the snippet to everyone who visits /terms . And the website will display the same content when existing users create new accounts from within the website.

Images

You can upload and store images by visiting SUPERUSER IMAGES.

You can name your images and use them on pages and snippets, by referincing their name like this:

<img src='{{ "myimage.png" | image }}'>

<!-- If you are not sure the image exists -->
{% assign img_url = "myimage.png" | image %}
{% if img_url != nil %}
  <img src="{{ img_url }}">
{% endif %}

The image filter will return nil ff the image name can not be recognized. For an overview of all available images, you can check the image_names variable.

Responding with JSON

Goexplore will respond in the same format as the request. If you need to respond to JSON requests, you can do so by formatting the HTML as a JSON hash. Examples of this:

<!-- A 204 response without a body -->
{"status": "204"}

<!-- A 422 response without a body -->
{"status": "422"}

<!-- A 200 response with {"foo": "bar"} as response body -->
{"status": "200", "foo": "bar"}

<!-- A {"status": "404"} response parsed from a hash -->
{{ nil | new_hash: status: "404" | hash_to_json }}

View API demo website .

Redirecting

If you want to redirect a regular HTTP request you can respond with json and include a url key. For example :

{"url": "{{ 'accounts__index' | path_for }}"}

If only a status key exists the response won't have a body. If the JSON can not be parsed, the response will contain it in plain text.

If you are receiving JSON requests from third party systems, and Goexplore interpretes them as HTML you can respond as described above, and include a format key with the value json.

Responding with Javascript

If you need to respond to Javascript requests, you can do so by putting Javascript code into the HTML, e.g.:

console.log("Hello world");
Background jobs

If you have long-running processes (more than 1 second) you may consider putting them into background jobs.

A background job is written in Liquid.

Your job will have access to the project variable that is described in the Creating pages section. Additionally you can pass the job any number of params when you create the job as follows.

{% assign create_task = "create_task" | create_job: company_id: account._id, name: "My task" %}

This will return e.g.:

{
  "job_id": "5094523e8a7993aac7cd326d"
}

If you have a job named create_task it will be executed in the background. The job could look like this:

{% liquid

  if params.company_id != nil

    assign create_task  = "create_task" | db_insert_one: company_id: params.company_id, name: params.name
    assign task_id      = create_task.document._id

  else

    assign error        = "No company exists"

  endif

%}

{"error": "{{ error }}", "task_id": "{{ task_id }}"}

You can check the job status like this:

{% assign job = job.job_id | get_job %}

Immediately after you created the job, its status is an empty string. Once a job has begun execution, it will get the status in_progress. The job will end as errored or finished. errored will be used if the job could not be executed, or if the JSON output of the job could not be parsed. An error_message key within the data key will in that case describe the error, and depending on the error type, other keys might be included.

A finished job could look like e.g.:

{
  "name": "create_task",
  "status": "finished",
  "data": {
    "error": "",
    "task_id": "5e62a1393ab42f19bce00c85"
  }
}

Jobs can execute queries and webhooks, and use locales for any purpose, but can not include snippets.

Sending emails

You can send emails from your own email address if you fill out the reqquired fields in the SMTP section. View email demo website .

You can send an email like this:

{% assign my_email = "my_email" | send_email: to: "example@example.com", display_name: "My company", foo: "bar" %}

In this example:

  1. The my_email string argument refers to the name of an email template. Visit SUPERUSER EMAILS to create it.
  2. The to argument determines who should receive the email.
  3. The display_name argument sets the display name in the recipients inbox. If you don't pass this argument, the default display name will be used.
  4. The foo argument can be accessed from within the email template as params.foo.

The email template could look like this:

{{ l.headline }}
<p>{{ l.foo }}: {{ params.foo }}</p>

The first line of the email will be used as subject of the email, and will not be included in the email body. The locale will be passed to the email based on the session from which the email was sent. But you can override it by passing a l argument to the email.

If you need to override the default SMTP settings, you can pass the following arguments: smtp_address, smtp_port, smtp_user_name, smtp_password and smtp_authentication. All arguments need to be included for the default SMTP settings to be overridden. You can optionally pass a smtp_reply_to address.

Sending the email will be a task carried out by a background job, so you will not immediately be able to check if the email was sent. Instead my_email variable will include a job_id like this:

{
  "job_id": "76b64969ac525cb3317729e7"
}

You can check the status of the job as described in the Background jobs section.

An email template has access to the arguments that you pass to it, but not much else. You can not query your database or execute webhooks. You can not include snippets. You also don't have access to the usual variables, only the project variable, which will only contain the _id attribute.

The names below are reserved for authentication purposes. If you create email templates with these names they will be used instead of our default templates, which are not styled or localized. If you override them, you should replicate their inner logic. Even if you have not registered SMTP details these emails will be sent. They will be sent from the address goexplore-robot@goaddon.com.

confirmation_instructions

Users receive a confirmation email when they create an account themselves, or get invited to join an existing account by other users. If the user is invited, the email will include a confirmation link, otherwise it will include a token that the user can submit. Our standard email looks like this:

Confirm your email
<p>Welcome {{ params.to }}!</p>
{% if params.superuser %}
  <p>
    <a href="{{ params.confirmation_url }}">Confirm your superuser</a>
  </p>
{% else %}
  {% if params.user.inviter_id != nil or params.user.encrypted_password.size != 0 %}
    <p>
      <a href="{{ params.confirmation_url }}">Confirm your account</a>
    </p>
  {% else %}
    <p>You can confirm your account with this token:</p>
    <p><code>{{ params.token }}</code></p>
  {% endif %}
{% endif %}

reset_password_instructions

Users can request an authentication token if they wish to reset their password. Example HTML:

Reset your password
<p>Hello {{ params.to }}!</p>
<p>You have requested to change your password. Your authentication token is:</p>
<p><code>{{ params.token }}</code></p>
<p>If you did not request this, please ignore this email. Your password will not change until you reset it.</p>

unlock_instructions

Users will be required to unlock their account if they submit incorrect passwords too many times. Example HTML:

Unlock your account
<p>Hello {{ params.to }}!</p>
<p>Your account has been locked due to an excessive number of unsuccessful sign in attempts.</p>
<p>
  <a href="{{params.unlock_url}}">Unlock your account</a>
</p>
General settings

Visit SUPERUSER SETTINGS to:

  • Customize the color of the left hand menu.
  • Define what page should be displayed as your frontpage.
  • Define what page users should see after successfull sign up.
  • Define what page should be the frontpage for authenticated users.
Apply terms to your website

Your website will require any user to accept your terms before they sign up. If new users are invited to manage an existing account, they will be required to accept your terms before they can access any of your pages. You can insert your terms into the acceptance page by creating a snippet called terms. We consider your terms to be static, and will therefore not evaluate them with Liquid.

The sign up page of your website will encourage new users to review your terms at the path /terms you should create a terms__index page that belongs to a container that does not require authentication. On that page you could include the terms snippet by including it like this:

{% include "terms" %}
Loading assets into the website

If you need to load scripts or stylesheets from an external CDN you can register their URL's in the Assets section.

Deleting accounts on your website

By default, users can create accounts, but not delete them. This is because you most likely have certain requirements and checks that you would like to conduct before allowing the account to be deleted. For example payment of outstanding bills. We have a standard delete page that users can use to request deletion of their account. You can activate it by visiting SUPERUSER SETTINGS.

When users request account deletion, the request will go to the accounts__destroy page. If you create this page you can implement your business logic and respond as you see fit. You could for example destroy the account, remove all user associations and redirect the user to / like this:

HTML

{% liquid
  assign destroy_account       = "destroy_account" | db_delete_one: _id: account._id
  assign account_id_attributes = "shadow_company_id company_id" | split: " "

  for account_id_attribute in account_id_attributes

    assign query          = nil | new_hash: app_id: project.app_id
    assign query          = query | push_to_hash: account_id_attribute, account._id
    assign document       = nil | new_hash
    assign document       = document | push_to_hash: account_id_attribute, nil
    assign revoke_access  = "revoke_access" | db_update_many: query: query, document: document

  endfor
%}

{"url": "/", "message": "{{ l.destroyed_account }}"}

JSON

{
  "destroy_account": {
    "klass": "companies",
    "query": {
      "_id": "{{ _id }}"
    }
  },
  "revoke_access": {
    "klass": "users",
    "query": "{{ query }}",
    "document": {
      "$unset": "{{ document }}"
    }
  }
}

Locales

{
  "en": {
    "destroyed_account": "The account was destroyed"
  }
}
Work offline and deploy from git

You will quickly find it inefficient to manage your content through our web interface.

Instead you can fork one of our demo repositories into your own account at Github, Bitbucket or similar. You can also create a repository from scratch, as exemplified in this video . Once you have implemented your own features into it you can visit SUPERUSER REPOSITORIES and deploy from your repository.

The repository is structured as follows.

- templates
  - containers
    # .html files
  - pages
    # .html files
  - snippets
    # .html files
  - emails
    # .html files
  - jobs
    # .html files
- queries
  - containers
    # .json files
  - pages
    # .json files
  - snippets
    # .json files
  - jobs
    # .json files
- webhooks
  - containers
    # .json files
  - pages
    # .json files
  - snippets
    # .json files
  - jobs
    # .json files
- locales
  - containers
    # .json files
  - pages
    # .json files
  - snippets
    # .json files
  - emails
    # .json files
  - jobs
    # .json files
- menu_entries
    # .json files
- images
  - backgrounds
    # .png files
  - logos
    # .png files
  - favicons
    # .ico files
  - menu_entries
    # .png files
  - others
    # .png, .jpeg, .jpg, .svg, .gif and .pdf files
- head.html
- head.json
- script.js
- script.json
- style.css
- style.json
- before_actions.json

templates

This is the destination for your HTML content . There are separate folders containers, pages, snippets, emails and jobs.

Inside these folders you can put HTML files. A file inside the pages folder should be named after its controller and action, e.g. tasks__index.html. Files inside the containers, snippets, jobs and emails folders should have the names you want to reference them by.

queries

This is the destination for your database queries . There are folders for containers, pages, snippets and jobs, but not emails.

Inside these folders you can put JSON files with the same names as the templates they should be paired with. A query file that should belong to the previously mentioned template file tasks__index.html should be named tasks__index.json and put inside the pages folder.

webhooks

This is the destination for your webhooks . It follows the same principles as described in the previous section about queries.

locales

This is the destination for your locales . It follows the same principles as described in the previous sections about queries and webhooks. There is also a folder for emails.

menu_entries

The menu_entries folder should contain JSON files, each containing a hash representing a menu entry . A file should be structured like this:

{
  "title": {
    "en": "Tasks"
  },
  "index": 0,
  "paths": [
    {
      "controller": "tasks",
      "action": "index"
    },
    {
      "controller": "tasks"
    }
  ]
}

images

This is the destination for your images.

images/backgrounds

  • Inside the backgrounds folder you can put an image named authentication.{format}. Replace {format} with png, jpeg, jpg or svg. The background image will be displayed to the left of the authentication page.
  • It should be a file with a width of max. 2000 px and a height of max. 1000 px.

images/logos

  • Inside the logos folder you can put an image named authentication.{format}. Replace {format} with png, jpeg, jpg or svg. The logo will be displayed on top of the authentication page.
  • It should be a file with a width of max. 300 px and a height of max. 100 px.

images/favicons

  • Inside the favicons folder you can put the favicon.ico file that should be displayed in the browser tab.

images/menu_entries

  • Inside the menu_entries folder you can put icons that should represent a menu entry. The icon should have the same name as the menu_entry it represents, so if the menu_entry is called tasks.html it should be matched with an image called tasks.png.
  • If you put in images named home.png or account.png they will override the icons of the default menu entries.
  • It should be a .png file with a width of exactly 150 px and a height of exactly 180 px.

images/others

  • Inside the others folder you can put files that you want use in your application by referencing {{ image_name | image }}.

head.html and head.json

These two files can be used to put content directly into the head of the website. The HTML file is for the actual content, and the JSON file is for localization, which should follow the same structure as any other localization file. One repository can have one of each file. The head content of a repository will only be inserted when the visited page belongs to it.

script.js and script.json

These two files can be used to create a script that can be fetched from the users client. The JS file is for the actual content, and the JSON file is for localization, which should follow the same structure as any other localization file. One repository can have one of each file. The content can only be requested from a page that belongs to the same repository.

style.css and style.json

These two files work as described for scripts, only with stylesheets.

Assign a shared container for pages that does not require authentication

If you are deploying multiple repositories, and more than one them puts content on unauthenticated pages, you may want to bring them into a shared container, for example to give them a shared top menu. In that case you can visit SUPERUSER SETTINGS.

The container of your choice can not include any snippets. If the container is part of a repository you should register the repository _id and container name, separated by /. So for example 5feb0ae783c3367bf0463c16/my_container. If the container has been created through the interface at SUPERUSER CONTAINERS you should simply register my_container.

If you embed pages into a shared container, a page will be embedded into its original container, and that original container will be embedded into the shared container.

The shared container will only be used if the request is in the format text/html.

Caching frequently used data

If you find yourself routinely reading fairly static data from your database or pulling from an external API, you may find it usefull to cache it for a given period like this:

{% assign minutes = 10 %}
{% assign cached  = "foo" | write_to_cache: "bar", minutes %}

This will return true if the data was successfully cached.

You can fetch from the cache like this:

{% assign cached = "foo" | fetch_from_cache %}

And finally, you can delete the cached data before its expiry like this:

{% assign deleted = "foo" | delete_from_cache %}

This will return true if the data was successfully deleted, and nil if no such key was found.

Additional Liquid filters

Throughout this WIKI we have described different filters that has been added to the standard Liquid filters . In addition, these filters are available:

business_days_after

Returns the date after a number of business days.

<!-- If today is Dec 31, 20: -->

{{ "now" | in_time_zone | business_days_after: 1 | date: "%b %d, %y" }}

<!-- Output: -->

Jan 2, 21

business_days_until

Returns the number of business days between two dates.

<!-- If today is a Friday: -->

{% liquid
    assign from = "now" | in_time_zone
    assign to   = from | days_after: 2
%}

{{ from | business_days_until: to }}

<!-- Output: -->

1

days_after

Returns the date after number of days.

<!-- If today is Dec 31, 20: -->

{{ "now" | in_time_zone | days_after: 1 | date: "%b %d, %y" }}

<!-- Output: -->

Jan 1, 21

days_between_dates

Returns the number of days between two days.

{% liquid
    assign from = "now" | in_time_zone
    assign to   = from | days_after: 1
%}

{{ from | days_between_dates: to }}

<!-- Output: -->

1

days_to_years_months_days

Returns an array of hashes. Each hash contains a unit and a number.

{{ 400 | days_to_years_months_days }}

<!-- Output: -->

[
  {
    "unit": "years",
    "number": 1
  },
  {
    "unit": "months",
    "number": 1
  },
  {
    "unit": "days",
    "number": 5
  }
]

decrypt

Uses your encryption key to decrypt a string, typically that is stored in your database.

{{ "84Mj7XVTtnooMrREVAcBbg==" | decrypt }}

<!-- Output: -->

"foo"

encrypt

Uses your encryption key to encrypt a string, typically before you store it in your database.

{{ "foo" | encrypt }}

<!-- Output: -->

"84Mj7XVTtnooMrREVAcBbg=="

generation_time

Returns the generation time of a MongoDB ObjectId.

{{ "6026dd2a83c33640cdef7037" | generation_time | date: "%Y-%m-%d %H:%M" }}

<!-- Output: -->

2021-02-12 19:55

hash_to_json

Converts a hash to JSON in a string.

{% assign hash = nil | new_hash: foo: "bar" %}

{{ hash | hash_to_json }}

<!-- Output: -->

{"foo": "bar"}

hours_after

Adds a number of hours to a date.

<!-- If today is 2021-01-01 00:00: -->

{{ "now" | in_time_zone | hours_after: 1 | date: "%Y-%m-%d %H:%M" }}

<!-- Output: -->

2021-01-01 01:00

i18n_l

Returns a localized strftime date.

<!--

Choose between the following:

en:

wday_date:               "%A %d/%m-%Y"
date_hours_minutes:      "%d/%m-%Y %H:%M"
wday_date_hours_minutes: "%A %d/%m-%Y %H:%M (%Z)"
date:                    "%d/%m-%Y"
date_pretty:             "%-d. %B %Y"
wday_date_pretty:        "%A, %B %-d"
wday:                    "%A"

-->

{{ "now" | in_time_zone | i18n_l: "wday_date_hours_minutes" }}

<!-- Output: -->

Friday 01/01-2021 21:03 +0100 (CET)

in_time_zone

Return the input date in a specific timezone. If you don't provide the timezone as an argument your default timezone will be used.

{{ "now" | in_time_zone: "Copenhagen" }}

<!-- Output is a ActiveSupport::TimeWithZone object: -->

2021-01-01 21:06:25 +0100

json_to_hash

Converts a string with valid JSON formatting to a hash.

{% assign json = "{}" %}

{{ json | json_to_hash }}

<!-- Output: -->

{}

klassname

Returns the class of the input.

{{ "foo" | klassname }}

<!-- Output (String, Float, TrueClass, FalseClass, Integer or Array, NilClass): -->

String

log

Creates a log entry that you can view in SUPERUSER LOG ENTRIES. The filter returns nil.

{{ "foo" | log }}

<!-- Output: -->

nil

new_hash

Initializes and returns an empty hash, or with the passed arguments.

{{ nil | new_hash: foo: "foo", bar: "bar" }}

<!-- Output: -->

{
  "foo": "foo",
  "bar": "bar"
}

new_object_id

Generates and returns a new MongoDB ObjectId.

{{ nil | new_object_id }}

<!-- Output: -->

6026dd2a83c33640cdef7037

new_uuid

Generates and returns a new uuid.

{{ nil | new_uuid }}

<!-- Output: -->

3fc339d2-3eb9-4354-a9fe-f318e813ff47

previous_business_day

Returns the previous business day, or today if today is a business day.

<!-- If today is Jan 2, 21: -->

{{ "now" | in_time_zone | previous_business_day | date: "%b %d, %y" }}

<!-- Output: -->

Jan 2, 21

push_to_array

Accepts two arguments, an array and the object that should be pushed to it. Returns the new array.

{% liquid
  assign array = "foo bar" | split: " "
  assign array = array | push_to_array: "baz"
%}

{{ array.size }}

<!-- Output: -->

3

push_to_hash

Accepts three arguments, a hash, a key and a value. Returns the new hash with the key/value pair.

{% assign hash = nil | new_hash %}

{{ hash | push_to_hash: "foo", "bar" }}

<!-- Output: -->

{
    "foo": "bar"
}

time_distance_in_words

Returns a string based on distance_of_time_in_words .

{% liquid
  assign from = "now" | in_time_zone
  assign to   = from | days_after: 1
%}

{{ from | time_distance_in_words: to }}

<!-- Output: -->

1 day

to_hash

Accepts two arguments, an array of hashes and a key name. Returns a new hash with each of the input arrays hashes in it.

{% liquid
  assign array = "" | split: " "
  assign foo   = nil | new_hash: key: "foo"
  assign bar   = nil | new_hash: key: "bar"
%}

{{ array | push_to_array: foo | push_to_array: bar | to_hash: "key" }}

<!-- Output: -->

{
  "foo": {
    "key": "foo"
  },
  "bar": {
    "key": "bar"
  }
}

to_hash_groups

Accepts two arguments, an array and a key name. Return a new hash, where all values are arrays. Each array contains all elements that share the same value of the specified key.

{% liquid
  assign array = "" | split: " "
  assign foo   = nil | new_hash: key: "foo"
  assign bar   = nil | new_hash: key: "bar"
%}

{{ array | push_to_array: foo | push_to_array: bar | push_to_array: bar | to_hash_groups: "key" }}

<!-- Output: -->

{
  "foo": [
    {
      "key": "foo"
    }
  ],
  "bar": [
    {
      "key": "bar"
    },
    {
      "key": "bar"
    }
  ]
}

Pricing

The price of running your website/app on Goexplore infrastructure is calculated based on different levels of activity.

All activity except traffic is pro-rated daily, although prices are shown per 30-day months. You will be billed through Goaddon.

Base fee

By running a website on Goexplore you incur a charge of 6.99 EUR/month. The first 6 months are free.

Account fee

A fee is calculated for each account that is registered to use your website. If you are only using the website internally with a single account, there is no account fee.

Number of accounts Price
0-1 Free
2-5 6.99 EUR/month
6-10 13.99 EUR/month
11+ 16.99 EUR/month + 0.2 EUR/month per account
Traffic fee

A daily fee is calculated based on the number of visits/requests made to your website. Most websites should not expect breach the limit of free requests. For example, a website receiving 1 request/second during the 8 peak hours of a day would not incur any traffic fees.

Requests per day Price
0-28,800 (eq. 20 requests/minute during a day) Free
28,800+ 0.002 EUR/1,000 requests

Important! Traffic fees are waived until further notice.

Price examples

An online shop hosting an internal app would only need one account, and is likely to use only a fraction of the free traffic. They would therefore only incur the base fee of 6.99 EUR/month.

A SaaS startup with 10 ecommerce companies using their app would additionally incur an account fee, bringing their cost up to 20.98 EUR/month (2.1 EUR/month per account):

Fee description Price/month
Base fee 6.99 EUR
Account fee 13.99 EUR

An established SaaS company with 100 ecommerce companies using their app would incur a higher account fee. If each account was contributing with 5 requests per minute during the 8 busiest hours of the day, the app would account for a total of 240,000 requests per day and therefore incur a traffic fee. In total this would bring their cost up to 56.65 EUR/month (0.57 EUR/month per account):

Fee description Price/month
Base fee 6.99 EUR
Account fee of 16.99 + 100 * 0.2 36.99 EUR
Traffic fee of (240,000 - 28,800) / 1,000 * 0.002 * 30 12.67 EUR

A larger SaaS company with 500 ecommerce companies using their app would account for 1,200,000 requests per day. They would incur a higher account fee, plus a traffic fee, bringing their cost up to 194.25 EUR/month (0.39 EUR/month per account):

Fee description Price/month
Base fee 6.99 EUR
Account fee of 16.99 + 500 * 0.2 116.99 EUR
Traffic fee of (1,200,000 - 28,800) / 1,000 * 0.002 * 30 70.27 EUR

A large SaaS company with 1,000 ecommerce companies using their app would account for 2,400,000 requests per day. They would incur an even higher account fee, plus a higher traffic fee, bringing their cost up to 359.26 EUR/month (0.36 EUR/month per account):

Fee description Price/month
Base fee 6.99 EUR
Account fee of 16.99 + 1,000 * 0.2 216.99 EUR
Traffic fee of (2,400,000 - 28,800) / 1,000 * 0.002 * 30 142.27 EUR

Terms of Service

This document was last revised 22-04-2022.

The Goexplore addon (“Goexplore” or “Addon”) is offered by Goaddon ApS (“Goaddon”, “we”, “us”, “our”). This agreement (“Addon Agreement”) is a supplement to the agreement (“Agreement”) between you (the “Account”) and Goaddon. When subscribing to the Addon, you enter the Account into this Addon Agreement and may use our services (“Addon Services”). The Agreement supersedes the Addon Agreement. Do not subscribe to the Addon unless you understand and agree to this Addon Agreement in its entirety.

1. Our Addon Services

Upon entering into the Addon Agreement the Account may access and use the Addon Services, constrained by the terms set forth by the Agreement and subsequently the Addon Agreement.

The Addon Services include (1) hosting of a website (“Website Hosting”) and (2) support (“Addon Support”).

If you subscribe the Account to this Addon, we will make available to the Account, server infrastructure on which you can upload the source code of the website and make the website accessible on the public internet. We will store data derived from usage of the website in the database of the Account. We will only hold data obtained during this process while using it to perform the Addon Services, and we will not store or backup the data ourselves. We will store the source code of the website unencrypted. Consequently you should not keep secrets directly in the source code. If the source code needs to use secrets you should rather register them through #{secrets_url} as described in our documentation. We will store the secrets in an encrypted environment and only make them accessible to the source code on the systems running the website. When you upload source code to our systems, we obtain copyrights and any other intellectual property rights to the source code itself, as well as texts, pictures, etc. ("IP"). You merely have a perpetual, but limited, non-exclusive license to use the IP on our systems while you are not in breach of the Agreement or the Addon Agreement. Thus, you accept that the IP may be used by us to provide Addon Services for other Accounts, or in relation to other projects of ours.

You are automatically subscribed to Addon Support, for which we calculate fees on a per-minute basis. We will provide you with Addon Support in accordance with the support policy described on the subscription page of this Addon.

2. No Free Support

Subscribing the Account to this Addon does not entitle you to free support. If the Account is not prepared to pay for Addon Support, we may refuse to address your support enquiries.

3. Storing your data

When you subscribe to Addon Services, Website Hosting will be carried out on servers located in the zone you have chosen for the Account. All data related to the Account will also be stored on servers in this zone. This includes (1) data shared by Goaddon with Goexplore, such as database credentials and data encryption keys and (2) secrets registered by you through #{secrets_url} .

Website Hosting SLA

Goaddon is committed to use commercially reasonable efforts to maximize the availability of the website hosted on Goexplore. This Service Level Agreement (“SLA”) applies only to websites that have been up for a minimum of 48 hours, and does not apply to any other service offered by Goaddon. We will give at least 14 days of advance notice for adverse changes to this SLA. Do not subscribe to the Addon unless you understand and agree to this SLA in its entirety.

Extended SLAs are available on request.

1. Definitions

In this SLA, the “Addon Agreement” refers to the parent agreement of this SLA, and "Agreement" refers to the parent agreement of the Addon Agreement. The Agreement supersedes the Addon Agreement, and the Addon Agreement supersedes this SLA.

The “Account” refers to the legal entity that is a part of the Agreement and the Addon Agreement with Goaddon.

The “Website” is the website of the Account that is hosted on Goexplore.

"Monthly Fees" means the total amount paid under the Account for hosting its Website during the month.

"Downtime" is calculated on a monthly basis and is the total number of minutes during the month that the entire Website was unavailable. A minute is considered unavailable if all of your continuous attempts to establish a connection to the Website within the minute fail. Downtime does not include partial minutes of unavailability or scheduled downtime for maintenance and upgrades.

Fee Deduction" is the percentage of the Monthly Fees to be credited to the Account if Goaddon approves your claim, as set forth in the table above.

"Monthly Uptime Percentage" is calculated on a monthly basis and is calculated as (total minutes in month - Downtime)/ total minutes in month * 100. If the Website is deployed for only part of the month it is calculated as 100% available for the minutes of the month where it is not deployed.

2. Our Obligations

If we do not achieve and maintain the Monthly Uptime Percentages set forth in the table below, you may be eligible for a Fee Deduction.

Monthly Uptime Percentage Fee Deduction
< 95% 10%
< 90% 25%
< 70% 100%

We do not commit to any particular response time in situations of downtime.

3. Account Obligations

To be eligible for a Fee Deduction:

  1. You must contact Goaddon within 24 hours of first becoming aware of an event that impacts service availability.
  2. You must submit your claim and all required information by the end of the month immediately following the month in which the Downtime occurred.
  3. You must include all information necessary for Goaddon to validate your claim, including: (i) a detailed description of the events resulting in Downtime, including logs from your attempts to establish a connection, as documentation of the errors and corroboration of your claimed outage (any confidential or sensitive information in the logs should be removed or replaced before shared with Goaddon); (ii) information about the time and duration of the Downtime; (iii) the number and physical location(s) of affected users (if applicable); and (iv) descriptions of your attempts to resolve the Downtime as it occurred.
  4. You must reasonably assist Goaddon in our investigation of the cause of the Downtime and our processing of your claim.
  5. You must comply with applicable Goexplore documentation and any advice from our support team.
4. Limitations

Downtime does not include, and the Account will not be eligible for a Fee Deduction for any performance or availability issue that is caused by:

  1. Factors outside of our reasonable control, including acts of God, labor disputes or other industrial disturbances, systemic electrical, telecommunications or other utility failures, breakdown of communication facilities, breakdown of internet service providers, cyber-attacks, earthquakes, storms, floods, fires, pandemics, quarantines, or other elements of nature, as well as blockages, embargoes, riots, acts or orders of government, acts of terrorism or war;
  2. Services, hardware, or software provided by a third party, such as cloud platform services on which the Website runs;
  3. Use of your password or equipment to access our systems; or
  4. Your or any third party’s (a) improper use or configuration of Website Hosting, or (b) failure to follow appropriate security practices.
5. Fee Deductions

We will process claims within 30 days of receipt. If we determine that you have met the Account Obligations above and that none of the stated Limitations apply to your claim, we will grant the Account a Fee Deduction.

We will apply any Fee Deduction to future invoices for the Website that experienced the Downtime.

Fee Deductions are the sole and exclusive remedy of the Account under this SLA.


Requirements & consequences for your database

When you subscribe to an addon, Goaddon provisions a database user that enables the addon to access your Goaddon database.

The addon will use this access to do whatever you subscribed for them to do.

At any point in time you can unsubscribe and revoke their database access.

The addon gets read/write access to the entire database. Below you can see what collections this particular addon reads from, writes to or indexes.

Please read carefully through the descriptions below. From it you should be able to determine if the addon uses MongoDB in a way that conflicts with your usage, but it is also where you can familiarize youself with what interesting data the addon contributes with.

For each attribute in a collection you can see if the addon is encrypting the data before saving it in MongoDB. If that is the case you can decrypt the attribute using the encryption keys associated with your account.

Description

Log entries from your traffic on Goexplore are stored in your own database. You can generate an entry with the Liquid filter log.

Attributes

Name Type Encrypted Description
_id BSON::ObjectId -
app_id String No The identifier of the app from which the logging occurred, e.g. 61c9cf58d4273f0007ca9d34-61ca0efad4273f0007ca9d36.
context String No The context in which the loggin occurred, e.g. app, job or mail.
data String Yes The logged data.

Indexes

Key Name Unique Sparse
{"app_id":1,"_id":-1} No No

Description

Users are used for authentication. Most user attributes are used internally by the website.

Attributes

Name Type Encrypted Description
_id BSON::ObjectId -
app_id String No
confirmation_sent_at Time - The email of the user, e.g. example@example.com.
confirmation_token String No
confirmed_at Time -
current_sign_in_at Time -
current_sign_in_ip String No
email String No
encrypted_password String No Hashed password chosen by the user.
failed_attempts Integer -
inviter_id BSON::ObjectId -
last_otp_at Integer -
last_sign_in_at Time -
last_sign_in_ip String No
locked_at Time -
otp Boolean -
otp_reset_requested_at Time -
otp_secret String No
otp_verified_ips Hash No
remember_created_at Time -
reset_password_sent_at Time -
reset_password_token String No
sign_in_count Integer -
superuser Boolean - Determines if the user has superuser access, or only has access to the account who has invited it.
superuser_level Integer - Either 1 which gives full superuser access, or 2 which enables the user to switch between registered accounts.
unconfirmed_email String No
unlock_token String No

Indexes

Key Name Unique Sparse
{"_id":1,"app_id":1,"superuser":1} No No
{"_id":1,"app_id":1} No No
{"app_id":1,"superuser":1} No No
{"email":1} Yes No


Intro

This API enables you to maintain your subscription programatically.

Each zone has its own API URL. Make sure to interact with the zone where your project belongs:

Zone URL
European Union (EU) https://goexplore-eu-api.goaddon.com
United States (US) https://goexplore-us-api.goaddon.com

Thoughout these docs the URL of the European Union zone will be used.

Authentication

All requests to the API must include a base64 encoded version of your Project API token .

If, for example, you want to update a project that has _id 5bf935bc3ab42fc4f4280d04, and your API token is 2595f9cc-1a4f-44e7-a7ea-0b55029533ad, then your request should look like this:

PATCH /project_api/v1/projects/5bf935bc3ab42fc4f4280d04
Host: https://goexplore-eu-api.goaddon.com
Content-Type: application/json
Accept: application/json
Authorization: Basic MjU5NWY5Y2MtMWE0Zi00NGU3LWE3ZWEtMGI1NTAyOTUzM2Fk
{
  "foo": "bar"
}

Unauthorized requests will responded with 401 Unauthorized.

Resources
projects

If you have subscribed with your project you can manage some of settings through this API.

Name Type Description

_id

BSON::ObjectId

locales

Array

To enable a language you need to include it in your array of allowed languages, e.g. ['en'].

Each time a user wants to access your website we will check if the language is allowed.

If it isn't we will serve the user the language at the top of your list.

url

String

URL which DNS you will point to our servers, e.g. example.com.

account_collection_name

String

Your preferred parent collection entity name, e.g. companies, organizations or accounts.

account_document_identifier

String

Unique parent identifier field name, e.g. website or name.

account_document_regex

String

A regular expression that the unique parent identifier field name should match, e.g. ^(?!w{3}.)[a-z0-9-]+.[a-z0-9-]+.*$.

account_document_title

Hash

A hash of locales for the title of the input field for the account identifier.

account_document_placeholder

Hash

A hash of locales for the placeholder of the input field for the account identifier.

account_document_explainer

Hash

A hash of locales for the explainer of the input field for the account identifier.

welcome_headline

Hash

A hash of locales for the headline of your welcome page

welcome_explainer

Hash

A hash of locales for the explainer on your welcome page

new_session_headline

Hash

A hash of locales for the headline on sign in option at the welcome page

new_session_explainer

Hash

A hash of locales for the explainer on sign in option at the welcome page

new_registration_headline

Hash

A hash of locales for the headline on sign up option at the welcome page

new_registration_explainer

Hash

A hash of locales for the explainer on sign up option at the welcome page

menu_color

String

The color of the left hand menu, e.g. 123456.

root_authenticated_to

String

The controller and index action where authorized users should be routed to, e.g. tasks__index.

root_unauthenticated_to

String

The controller and index action where unauthorized visitors should be routed to, e.g. static_pages__index.

redirect_confirmed_to

String

The controller and index action where new users should be redirected to, e.g. onboarding__index.

unauthenticated_container

String

The path of the container that should be used for all unauthenticated pages, e.g. 5feb0ae783c3367bf0463c16/unauthenticated.

enable_delete_account

Boolean

This determines wether there should be a simple account deletion page on the website. The delete button will make a request to accounts__destroy, a page that you should create yourself.

scripts

Array

If your website require any external stripts to load, you can provide an array of script URL's. jQuery 3.6.0, Bootstrap 5.1.1 and Ace 1.4.12 are already loaded. E.g. ['https://cdn.example.com.min.js'].

styles

Array

If your website require any external stylesheets to load, you can provide an array of stylesheet URL's. Bootstrap 5.1.1 and FontAwesome 5.4.2 are already loaded. E.g. ['https://cdn.example.com.min.css'].

whitelisted_emails

Array

By default your website allows anyone to sign up for a new account. However, if the features of your website is only meant to be accessible by intivation only, you can whitelist the email addresses that should be allowed in. If you want everyone from a specific domain to whitelisted, you can register the domain or subdomain.

framework_id

BSON::ObjectId

The framework that your website should be deployed on.

otp_name

String

Your display name when your website is added to a 2FA authenticator app.

otp_expiry_days

Integer

The number of days a 2FA verification is valid. A successfull verification upon a login does not expire, but if you choose a expiry period greater then 0 days the user will be able to sign out and back in from the same IP address without being asked for 2FA verification within the chosen number of days. Once the expiration period has ended the user will be asked to verify their identity once again, even if they are already signed in.

Description

Updates your project.

Request

Include any of the following keys in your JSON body:

{
  "locales": ["en"],
  "account_document_title": {
    "en": "Website"
  },
  "account_document_placeholder": {
    "en": "E.g. example.com"
  },
  "account_document_explainer": {
    "en": "Write e.g. example.com."
  },
  "welcome_headline": {
    "en": "Welcome"
  },
  "welcome_explainer": {
    "en": "How would you like to proceed?"
  },
  "new_session_headline": {
    "en": "Sign in"
  },
  "new_session_explainer": {
    "en": "For existing users"
  },
  "new_registration_headline": {
    "en": "Create account"
  },
  "new_registration_explainer": {
    "en": "For new users"
  },
  "url": "your-project.example.com",
  "account_collection_name": "companies",
  "account_document_identifier": "website",
  "account_document_regex": "^(?!w{3}\.)[a-z0-9\-]+\.[a-z0-9\-]+.*$",
  "menu_color": "123456",
  "root_authenticated_to": "tasks__index",
  "root_unauthenticated_to": "static_pages__index",
  "redirect_confirmed_to": "onboarding__index",
  "unauthenticated_container": "5feb0ae783c3367bf0463c16/unauthenticated",
  "enable_delete_account": true,
  "scripts": [
    "https://cdn.example.com.min.js"
  ],
  "styles": [
    "https://cdn.example.com.min.css"
  ],
  "whitelisted_emails": [
    "example@example.com",
    "sub.example.com"
  ]
}
HTTParty.patch(
  "https://goexplore-eu-api.goaddon.com/project_api/v1/projects/5c0007cb3ab42fc4f4280d0a",
  headers: {
    "Authorization" => "Basic #{Base64.strict_encode64('2595f9cc-1a4f-44e7-a7ea-0b55029533ad')}",
    "Content-Type"  => "application/json"
  },
  body: body.to_json
)
Response
Name Description
success

A text confirmation describing if the project was successfully updated.

message

A text confirmation describing if the project was recognized, but that there was no new data to update with.

Name Description
error

A text description of the error.

No response body.

secrets

You can manage your secrets through this endpoint.

Description

Creates a secret.

Request

Include any of the following keys in your JSON body:

{
  "key": "foo",
  "value": "bar"
}
HTTParty.post(
  "https:///project_api/v1/secrets",
  headers: {
    "Authorization" => "Basic #{Base64.strict_encode64('2595f9cc-1a4f-44e7-a7ea-0b55029533ad')}",
    "Content-Type"  => "application/json"
  },
  body: body.to_json
)
Response
Name Description
success

A text confirmation of the secret being created.

Name Description
error

A text description of the error.

No response body.

Description

Destroys a secret.

Request

No request body

HTTParty.delete(
  "https:///project_api/v1/secrets/foo",
  headers: {
    "Authorization" => "Basic #{Base64.strict_encode64('2595f9cc-1a4f-44e7-a7ea-0b55029533ad')}",
    "Content-Type"  => "application/json"
  }
)
Response
Name Description
message

A text confirmation of the secret being destroyed.

Name Description
error

A text description of the error.

No response body.