Goexplore- by Goaddon |
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.
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.
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 } } ]
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.
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.
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
:
/tasks/new
goes to the new
action, so /tasks/new ./tasks/:id
goes to the show
action, so e.g. /tasks/5d0234903ab42fd9483a4807 ./tasks/:id/:action
goes to a custom action, so e.g. /tasks/5d0234903ab42fd9483a4807/refund ./tasks
goes to the index
action, so /tasks ./tasks
goes to the create
action./tasks/:id
goes to the update
action./tasks/:id
goes to the destroy
action.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:
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.
The title of the menu entry should be chosen for each language. If you only support the en
language it could be:
{ "en": "Tasks" }
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 .
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.
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:
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.
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" }
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.
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 %}
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.
A page can belong to a container, as descriped in the next section.
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.
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.
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.
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.
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.
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.
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 }}
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
.
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");
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.
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:
my_email
string argument refers to the name of an email template. Visit SUPERUSER EMAILS to create it.to
argument determines who should receive the email.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.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>
Visit SUPERUSER SETTINGS to:
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" %}
If you need to load scripts or stylesheets from an external CDN you can register their URL's in the Assets section.
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" } }
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
images/logos
images/favicons
images/menu_entries
tasks.html
it should be matched with an image called tasks.png
.home.png
or account.png
they will override the icons of the default menu entries.images/others
{{ 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.
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
.
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.
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" } ] }
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.
By running a website on Goexplore you incur a charge of 6.99 EUR/month. The first 6 months are free.
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 |
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.
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 |
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.
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.
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.
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} .
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.
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.
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.
To be eligible for a Fee Deduction:
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:
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.
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 |
A shortcut is often the best and fastest way to configure an addon. Some shortcuts are used to integrate your addon subscriptions with each other. The following shortcuts require a subscription to this addon.
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.
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
.
If you have subscribed with your project you can manage some of settings through this API.
Name | Type | Description |
---|---|---|
|
BSON::ObjectId |
|
|
Array |
To enable a language you need to include it in your array of allowed languages, e.g. 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. |
|
String |
URL which DNS you will point to our servers, e.g. |
|
String |
Your preferred parent collection entity name, e.g. |
|
String |
Unique parent identifier field name, e.g. |
|
String |
A regular expression that the unique parent identifier field name should match, e.g. ^(?!w{3}.)[a-z0-9-]+.[a-z0-9-]+.*$. |
|
Hash |
A hash of locales for the title of the input field for the account identifier. |
|
Hash |
A hash of locales for the placeholder of the input field for the account identifier. |
|
Hash |
A hash of locales for the explainer of the input field for the account identifier. |
|
Hash |
A hash of locales for the headline of your welcome page |
|
Hash |
A hash of locales for the explainer on your welcome page |
|
Hash |
A hash of locales for the headline on sign in option at the welcome page |
|
Hash |
A hash of locales for the explainer on sign in option at the welcome page |
|
Hash |
A hash of locales for the headline on sign up option at the welcome page |
|
Hash |
A hash of locales for the explainer on sign up option at the welcome page |
|
String |
The color of the left hand menu, e.g. |
|
String |
The controller and index action where authorized users should be routed to, e.g. |
|
String |
The controller and index action where unauthorized visitors should be routed to, e.g. |
|
String |
The controller and index action where new users should be redirected to, e.g. |
|
String |
The path of the container that should be used for all unauthenticated pages, e.g. |
|
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. |
|
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. |
|
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. |
|
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. |
|
BSON::ObjectId |
The framework that your website should be deployed on. |
|
String |
Your display name when your website is added to a 2FA authenticator app. |
|
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 |
Updates your project
.
You can manage your secrets through this endpoint.
Creates a secret.
Destroys a secret.