Introduction

Para is a flexible backend service, created as an open-source project from the very beginning, in 2013. It was born out of our need to have a robust system which would allow us to persist objects easily to anything - RDBMS, NoSQL and in-memory databases. We needed a simple solution with an API which would scale well and provide a solid foundation for our future projects.

Para is a stateless, schemaless, 3-layer backend system with a REST API in front of it. The first layer is the database, the second layer is the search index and the third – the cache. Depending on how you use it, Para can either be a standalone backend service or a persistence framework that is part of your code base. Each request to the API is stateless, meaning you can scale out easily. The data model requires no schema – it’s based around plain old Java/JSON objects and is optimized for schemaless key-value data stores, but also works with traditional databases.

Para is also multitenant, which means you can deploy it as a standalone service on one or more nodes and host one or more applications on it (“apps”). An app can be a website, mobile app, desktop app or even a command-line tool. This is made possible by the REST API which talks JSON to your apps, and with the help of the client libraries below, it’s easy to get started. If you’re building an application on the JVM, you can also add Para as Maven dependency to your project. You can still keep the REST API or turn it off completely.

Quick start

  1. Download the latest executable JAR
  2. Create a configuration file application.conf file in the same directory as the JAR package.
  3. Start Para with java -jar -Dconfig.file=./application.conf para-*.jar
  4. Install Para CLI with npm install -g para-cli
  5. Create a new dedicated app for your project and save the access keys:
    # run setup and set endpoint to either 'http://localhost:8080' or 'https://paraio.com'
    # the keys for the root app are inside application.conf
    $ para-cli setup
    $ para-cli new-app "myapp" --name "My App"
    Alternatively, you can use the Para Web Console to manage data, or integrate Para directly into your project with one of the API clients below.

Users are created either programmatically with paraClient.signIn(...) or with an API request to POST /v1/jwt_auth. See Sign in or Authentication sections for more details.

Docker

Tagged Docker images for Para are located at erudikaltd/para on Docker Hub. It’s highly recommended that you pull only release images like :1.45.1 or :latest_stable because the :latest tag can be broken or unstable. First, create an application.conf file and a data folder and start the Para container:

$ touch application.conf && mkdir data
$ docker run -ti -p 8080:8080 --rm -v $(pwd)/data:/para/data \
  -v $(pwd)/application.conf:/para/application.conf \
  -e JAVA_OPTS="-Dconfig.file=/para/application.conf" erudikaltd/para:latest_stable

Environment variables

JAVA_OPTS - Java system properties, e.g. -Dpara.port=8000 BOOT_SLEEP - Startup delay, in seconds

Plugins

To use plugins, create a new Dockerfile-plugins which does a multi-stage build like so:

# change X.Y.Z to the version you want to use
FROM erudikaltd/para:v1.XY.Z-base AS base
FROM erudikaltd/para-search-lucene:1.XY.Z AS search
FROM erudikaltd/para-dao-mongodb:1.XY.Z AS dao
FROM base AS final
COPY --from=search /para/lib/*.jar /para/lib
COPY --from=dao /para/lib/*.jar /para/lib

Then simply run $ docker build -f Dockerfile-plugins -t para-mongo .

Maven

The Java client for Para is a separate module with these Maven coordinates:

<dependency>
  <groupId>com.erudika</groupId>
  <artifactId>para-client</artifactId>
  <version>{VERSION}</version>
</dependency>

In your own project you can create a new ParaClient instance like so:

ParaClient pc = new ParaClient(accessKey, secretKey);
// Para endpoint - http://localhost:8080 or https://paraio.com
pc.setEndpoint(paraServerURL);
// Set this to true if you want ParaClient to throw exceptions on HTTP errors
pc.throwExceptionOnHTTPError(false);
// send a test request - this should return a JSON object of type 'app'
pc.me();

We’ve built a full-blown StackOverflow clone with Para in just about 4000 lines of code - check it out at https://scoold.com

Client libraries

Building Para

Para can be compiled with JDK 8 and up.

To compile it you’ll need Maven. Once you have it, just clone and build:

$ git clone https://github.com/erudika/para.git && cd para
$ mvn install -DskipTests=true

To generate the executable “uber-jar” run $ mvn package and it will be in ./para-jar/target/para-x.y.z-SNAPSHOT.jar. Two JAR files will be generated in total - the fat one is a bit bigger in size.

To run a local instance of Para for development, use:

$ mvn spring-boot:run

Alternatively, you can build a WAR file and deploy it to your favorite servlet container:

$ cd para-war && mvn package

Standalone mode

There are two ways to run Para as a standalone server. The first one is by downloading the executable JAR file and executing it:

java -jar para-X.Y.Z.jar

The JAR contains an embedded Jetty server and bundles together all the necessary libraries. This is the simplest and recommended way to run Para.

Running a standalone server allows you to build a cluster of distributed Para nodes and connect to it through the REST API. Here’s a simple diagram of this architecture:


+-------------------------------------+
|  Your app + Para API client library |
+------------------+------------------+
                   |
+------------------+------------------+
|        REST API over HTTPS          |
+-------------------------------------+
|       Cluster Load Balancer         |
+------------------+------------------+
                   |
     +------------------ ... ----+
     |             |             |
+----+----+   +----+----+   +----+----+
| Para #1 |   | Para #2 |   | Para #N |
+---------+   +---------+   +---------+

  Node 1        Node 2   ...  Node N

Deploying to a servlet container or a self-hosted environment

Another option is to build and deploy the WAR file to a servlet container like Tomcat or GlassFish, for example.

Note: We recommend deploying the Para at the root context /. You can do this by renaming the WAR file to ROOT.war before deploying. See the Config for more details about configuring your deployment.

Para can also be deployed easily to a PaaS environment like Heroku or AWS Elastic Beanstalk. The JAR file is executable and should “just work” by setting the execution command to java -jar para-X.Y.Z.jar.

In a self-hosted environment where you want to manage your own SSL certificates, it is recommended to run a reverse-proxy server like NGINX in front of Para. As an alternative you can use Apache or Lighttpd.

Example configuration for NGINX

server_tokens off;
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options nosniff;
server { listen 80 default_server; listen [::]:80 default_server; server_name www.domain.com domain.com;
# Redirect all HTTP requests to HTTPS with a 301 Moved Permanently response. return 301 https://$host$request_uri; }
server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name www.domain.com domain.com;
# certs sent to the client in SERVER HELLO are concatenated in ssl_certificate ssl_certificate /path/to/signed_cert_plus_intermediates; ssl_certificate_key /path/to/private_key; ssl_session_timeout 1d; ssl_session_cache shared:SSL:50m; ssl_session_tickets off;
# modern configuration. tweak to your needs. ssl_protocols TLSv1.2; ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256'; ssl_prefer_server_ciphers on;
# HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months) add_header Strict-Transport-Security max-age=15768000;
# OCSP Stapling - fetch OCSP records from URL in ssl_certificate and cache them ssl_stapling on; ssl_stapling_verify on;
# Verify chain of trust of OCSP response using Root CA and Intermediate certs ssl_trusted_certificate /path/to/root_CA_cert_plus_intermediates;
# Cloudflare DNS resolver 1.1.1.1;
# Required for LE certificate enrollment using certbot location '/.well-known/acme-challenge' { default_type "text/plain"; root /var/www/html; }
location / { proxy_pass http://localhost:8080; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; proxy_set_header Host $http_host; } }

As an alternative, you can enable SSL and HTTP2 directly in Para:

  1. Run the script gencerts.sh to generate the required self-signed certificates

    echo "para.local" | sudo tee -a /etc/hosts
    ./gencerts.sh para.local secret

    The result of that command will be 8 files - ParaRootCA.(crt,key,pem), para.local.(crt,key,pem) as well as a Java Keystore file para-keystore.p12 and a Truststore file para-truststore.p12. Optionally, you can run generate the server certificates using an existing RootCA.pem and RootCA.key files like so:

    ./gencerts.sh para.local secret /path/to/ca/RootCA
  2. Run Para using the following command which enables SSL and HTTP2:

    java -jar -Dconfig.file=./application.conf \
    -Dserver.ssl.key-store-type=PKCS12 \
    -Dserver.ssl.key-store=para-keystore.p12 \
    -Dserver.ssl.key-store-password=secret \
    -Dserver.ssl.key-password=secret \
    -Dserver.ssl.key-alias=para \
    -Dserver.ssl.enabled=true \
    -Dserver.http2.enabled=true \
    para-*.jar
  3. Trust the root CA file ParaRootCA.crt by importing it in you OS keyring or browser (check Google for instructions).

  4. Open https://para.local:8000


Para also supports mTLS (mutual authentication) for Java clients.

Command to enable TLS, HTTP2 and mTLS.

java -jar -Dconfig.file=/para/application.conf \
 -Dserver.ssl.key-store-type=PKCS12 \
 -Dserver.ssl.key-store=para-keystore.p12 \
 -Dserver.ssl.key-store-password=secret \
 -Dserver.ssl.key-password=secret \
 -Dserver.ssl.trust-store=para-truststore.p12 \
 -Dserver.ssl.trust-store-password=secret \
 -Dserver.ssl.key-alias=para \
 -Dserver.ssl.client-auth=need \
 -Dserver.ssl.enabled=true \
 -Dserver.http2.enabled=true
para-*.jar

The Para client for Java should be configured appropriately:

para.client.ssl_keystore = "/path/to/client-keystore.p12"
para.client.ssl_keystore_password = secret
para.client.ssl_truststore = "/path/to/client-truststore.p12"
para.client.ssl_truststore_password = secret

Visit the releases page for the latest package.

Hosted service

Don’t want to deal with servers and performance issues? We offer hosting and premium support at ParaIO.com where you can try Para online with a free developer account. Browse and manage your users and objects, do backups and edit permissions with a few clicks in the web console. By upgrading to a premium account you will be able to scale you projects up and down in seconds and manage multiple apps.

Maven

Para is hosted on Maven Central. To add it to your project just include this into your pom.xml:

<dependency>
  <groupId>com.erudika</groupId>
  <artifactId>para-server</artifactId>
  <version>{VERSION}</version>
</dependency>

For building lightweight client-only applications connecting to Para, include only the client module:

<dependency>
  <groupId>com.erudika</groupId>
  <artifactId>para-client</artifactId>
  <version>{VERSION}</version>
</dependency>

By building your JVM app on top of Para, you have full control over persistence, caching and indexing operations. Here’s a simple diagram of this architecture:

+----------+ +----------+ +-----------+
| Web SPA  | |API Client| | Mobile app|
+----+-----+ +-----+----+ +----+------+
     |             |           |
+----+-------------+-----------+------+
|        REST API over HTTPS          |
+------------------+------------------+
|       Cluster Load Balancer         |
+------------------+------------------+
                   |
     +------------------ ... ----+
     |             |             |
+----+----+   +----+----+   +----+----+
| Your app|   | Your app|   | Your app|
+---------+   +---------+   +---------+
| Cache   |   | Cache   |   | Cache   |  \
+---------+   +---------+   +---------+   \
| Search  |   | Search  |   | Search  |    } Para
+---------+   +---------+   +---------+   /
| Database|   | Database|   | Database|  /
+---------+   +---------+   +---------+

  Node 1        Node 2   ...  Node N

Javadocs

para-core   para-server   para-client

The ParaObject interface

All domain classes in Para implement the ParaObject interface which gives objects basic common properties like id, timestamp, name, etc. Let’s say you have a plain old Java object like this:

class User {
    public String name;
    public int age;
}

and you want to turn it into persistable Para objects. You can do it by just adding one annotation and implementing ParaObject:

class User implements ParaObject {
    @Stored public String name;
    @Stored public int age;
}

This allows you do call create(), update(), delete() on that object and enables search indexing automatically. Now you can do:

User u = new User();
u.setName("Gordon Freeman");
u.setAge(40);
// generates a new id and persists the object
String id = u.create();

Once the objects is created and persisted we can search for it like this:

// returns a list of users found
List<User> users = Para.getSearch().findQuery(u.getType(), "freeman");

And here’s what a Para object looks like as JSON when returned from the REST API:

{
  "id" : "572040968316915712",
  "timestamp" : 1446469779546,
  "type" : "user",
  "appid" : "para",
  "updated" : 1446469780024,
  "name" : "Gordon Freeman",
  "votes" : 0,
  "identifier" : "fb:1000123456789",
  "groups" : "admins",
  "active" : true,
  "email" : "[email protected]",
  "objectURI" : "/users/572040968316915712",
  "plural" : "users"
}

Implementing your own ParaObject classes

This step is optional. There was a question about how to implement ParaObject and where to start from. First of all, there is no need to write your own custom classes if you’re going to be using them for simple stuff. So step one is to take a look at the generic class called Sysprop. Look at the source code on GitHub. It’s pretty simple and implements all of ParaObject‘s methods. Then you have to decide if you can work with that generic class or not. If you really need custom properties and methods then that class is a good starting point. Just copy the getters and setters and add your own fields and methods.

Another option is to extend Sysprop like so:

public class MyParaObject extends Sysprop implements ParaObject {
    // 'implements ParaObject' is redundant in this case

    @Stored
    public String myCustomField;
    // this field is ignored and not stored anywhere
    public transient String secretKey;

    // TODO: write getters & setters...
}

This is a quick way of having your own classes that work with Para and it spares you the writing of all the boilerplate code. You can later override the parent methods, for example if you need to execute some custom code on create():

@Override
public String create() {
    // TODO; write your own code here
    return dao.create(getAppid(), this); // this writes to DB
}

Fine-tuning backend operations

From version 1.18 Para objects have three new flags - stored, indexed and cached. These flags turn on and off the three main operations - persistence, indexing and caching. Developers can choose to switch off caching on a number of objects that change very often, for example. Also some objects my be hidden from search by setting the indexed: false flag. And finally you can turn off persistence completely with stored: false and thus have objects that live only in memory (and search index) but are never stored in the database.

Para Configuration

Once deployed, Para will initialize itself by loading the default configuration file reference.conf. To load your own configuration you need to set one of the following system properties:

  • config.resource specifies a resource name - not a basename, i.e. ‘application.conf’ and not ‘application’
  • config.file specifies a filesystem path, again it should include the file extension
  • config.url specifies the URL from which to load the config file

You need to set these properties before calling Para.initialize() or at the startup of your servlet container. If you have a file called application.conf or application.json on the classpath, Para will pick it up automatically.

Here’s a sample application.conf file:

para.env = "production"
para.print_logo = true
para.port = 8080

The configuration logic is to read system properties first, then environment properties and finally check the config file. So the order of precedence is as follows:

System.getProperty() > System.getenv() > application.conf

Note that environment properties containing . (dots) are invalid and will be replaced with _ (underscore). For example, the environment property $para_env is equivalent to para.env in the config file.

Important: In a production environment, set para.app_secret_key to some randomly generated secret string. This is important for securing your server because this secret key is used for signing and verifying authentication tokens.

List of ALL Para configuration properties

List of ALL Para configuration properties

Core

Property key & Description Default Value Type
para.app_name
The formal name of the web application.
para String
para.context_path requires restart
The context path (subpath) of the web application, defaults to the root path /.
String
para.port requires restart
The network port of this Para server. Port number should be a number above 1024.
8080 Integer
para.dao requires restart
Selects the DAO implementation at runtime. Can be AWSDynamoDAO, MongoDBDAO, CassandraDAO, etc. Each implementation has its own configuration properties.
H2DAO String
para.search requires restart
Selects the Search implementation at runtime. Can be LuceneSearch, ElasticSearch, etc.
LuceneSearch String
para.cache requires restart
Selects the Cache implementation at runtime. Can be one of CaffeineSearch, HazelcastCache.
CaffeineSearch String
para.q requires restart
Selects the Queue implementation at runtime. Can be one of LocalQueue, AWSQueue.
LocalQueue String
para.fs requires restart
Selects the FileStore implementation at runtime. Can be one of LocalFileStore, AWSFileStore.
LocalFileStore String
para.emailer
Selects the Emailer implementation at runtime. Can be one of AWSEmailer, JavaMailEmailer, NoopEmailer.
NoopEmailer String
para.search_enabled
Enable/disable full-text search functionality.
true Boolean
para.cache_enabled
Enable/disable object caching. Enabled in production mode by default.
false Boolean
para.webhooks_enabled
Enable/disable webhooks functionality using Webhook objects. Requires a queue.
false Boolean
para.api_enabled
Enable/disable the Para RESTful API.
false Boolean
para.cluster_name
The name of the Para cluster. Used by some of the plugins to isolate deployments.
para-prod String
para.core_package_name
The package path (e.g. org.company.app.core) where all domain classes are defined. Specify this when integrating your app with Para core/client, to get deserialization working.
String
para.admin_ident
The identifier of the first administrator (can be email or social login ID).
String
para.worker_id
Node number, 1 to 128. Used mainly for ID generation.Each instance of Para should have a unique worker id.
1 String
para.executor_threads
The number of threads to use for the ExecutorService thread pool.
2 Integer
para.max_failed_webhook_attempts
The number of maximum failed webhook delivery attemts. Webhooks with too many failed deliveries will be disabled automatically.
10 Integer
para.reindex_batch_size
Controls the number of documents to reindex in a single batch. By default is equal to page size for reading the docs from DB.
100 Integer
para.sync_index_with_db
Enable/disable the data synchronization between database and search index.
true Boolean
para.read_from_index
Enable/disable reading data from search index instead of database. Used for data recovery.
false Boolean
para.max_datatypes_per_app
Maximum number of data types which can be defined in each Para app.
256 Integer
para.max_entity_size_bytes
Maximum size (in bytes) of incoming JSON payload entities in requests to the API.
1048576 Integer
para.health.check_interval
The health check interval, in seconds.
60 Integer
para.health_check_enabled
Enable/disable the health check functionality in Para.
true Boolean

Basic Authentication

Property key & Description Default Value Type
para.fb_app_id
Facebook OAuth2 app ID.
String
para.fb_secret
Facebook app secret key.
String
para.gp_app_id
Google OAuth2 app ID.
String
para.gp_secret
Google app secret key.
String
para.in_app_id
LinkedIn OAuth2 app ID.
String
para.in_secret
LinkedIn app secret key.
String
para.tw_app_id
Twitter OAuth app ID.
String
para.tw_secret
Twitter app secret key.
String
para.gh_app_id
GitHub OAuth2 app ID.
String
para.gh_secret
GitHub app secret key.
String
para.ms_app_id
Microsoft OAuth2 app ID.
String
para.ms_secret
Microsoft app secret key.
String
para.ms_tenant_id
Microsoft OAuth2 tenant ID.
common String
para.az_app_id
Amazon OAuth2 app ID.
String
para.az_secret
Amazon app secret key.
String
para.sl_app_id
Slack OAuth2 app ID.
String
para.sl_secret
Slack app secret key.
String
para.mm_app_id
Mattermost OAuth2 app ID.
String
para.mm_secret
Mattermost app secret key.
String

Security

Property key & Description Default Value Type
para.cors_enabled
Enable/disable the CORS filter. It adds CORS headers to API responses.
true Boolean
para.security.csrf_protection
Enable/disable CSRF protection which checks for valid CSRF tokens in write requests.
true Boolean
para.security.csrf_cookie
The name of the CSRF cookie.
para-csrf-token String
para.auth_cookie
The name of the authorization cookie.
para-auth String
para.request_expires_after
Expiration period for signed API request, in seconds.
900 Integer
para.jwt_expires_after
Expiration period for JWTs (access token), in seconds.
86400 Integer
para.jwt_refresh_interval
JWT refresh interval, after which a new token is issued, in seconds.
3600 Integer
para.id_token_expires_after
Expiration period for short-lived ID tokens, in seconds.
60 Integer
para.session_timeout
Expiration period for the login session, in seconds.
86400 Integer
para.min_password_length
The minimum length of user passwords.
8 Integer
para.pass_reset_timeout
The time window in which passwords can be reset, in seconds. After that the token in the email expires.
1800 Integer
para.max_pass_matching_attemts
The maximum number of passord matching attempts for user accounts per time unit. After that the account is locked and user cannot login until the lock has expired.
20 Integer
para.pass_matching_lock_period_hours
The time to force a user to wait until they can try to log back in, in hours.
1 Integer
para.returnto_cookie
The name of the cookie used to remember which URL the user requested and will be redirected to after login.
para-returnto String
para.support_email
The email of the webmaster/support team. Para will send emails to this email.
[email protected] String
para.security.allow_unverified_emails
Enable/disable email verification after the initial user registration. Users with unverified emails won’t be able to sign in, unless they use a social login provider.
false Boolean
para.security.protected
Protects a named resource by requiring users to authenticated before accessing it. A protected resource has a {name} and value like this [\"/{path}\", \"/{path}/**\", [\"{role}\" or {http_method}]]. The value is an array of relative paths which are matche by an ANT pattern matcher. This array can contain a subarray which lists all the HTTP methods that require authentication and the user roles that are allowed to access this particular resource. No HTTP methods means that all requests to this resource require authentication.
ConfigObject
para.security.signin
The path to the login page.
/signin String
para.security.signin_success
The default page to send users to when they login.
/ String
para.security.signin_failure
The default page to send users to when login fails.
/signin?error String
para.security.signout
The path to the logout page.
/signout String
para.security.signout_success
The default page to send users to when they logout.
/signin String
para.security.access_denied
The path to redirect to when 403 code is returned.
/403 String
para.security.returnto
The path to return to when an authentication request succeeds.
returnto String
para.security.remember_me deprecated
Enable/disable remember me functionality.
true Boolean
para.app_secret_key deprecated
Salt.
md5('paraseckey') String

River & Queue

Property key & Description Default Value Type
para.default_queue_name
The name of the queue used by Para.
para-default String
para.queue_link_enabled
Enable/disable polling the queue for message. This controls the ‘river’ feature in Para.
false Boolean
para.queue.polling_sleep_seconds
60 Integer
para.queue.polling_interval_seconds
The polling interval of the Para river, in seconds. Polls queue for messages.
10 Integer
para.river.max_indexing_retries
The maximum number of attempts at reading an object from database and indexing it, when the operation was received from the queue.
5 Integer
para.indexing_sync_interval_sec
The time interval between the sending of each batch of index synchronization messages to the queue, in seconds.
10 Integer

Metrics

Property key & Description Default Value Type
para.metrics_enabled
Enable/disable the built-in metrics around CRUD methods.
true Boolean
para.metrics.logging_rate
The rate at which the metrics logger will write to file, in seconds.
60 Integer
para.metrics.graphite.host
The URL of the Graphite host to push metrics to.
String
para.metrics.graphite.port
The port number of the Graphite server.
2003 Integer
para.metrics.graphite.prefix_system
String
para.metrics.graphite.prefix_apps
The prefix to apply to metric names, e.g. com.erudika.para..
String
para.metrics.graphite.period
The period for how often to push system metrics in seconds. Disabled by default.
0 Integer
para.metrics.jmx_enabled
Enable/disable JMX reporting for all metrics.
false Boolean

LDAP Authentication

Property key & Description Default Value Type
para.security.ldap.password_param
LDAP password parameter name.
password String
para.security.ldap.username_param
LDAP username parameter name.
username String

File Storage

Property key & Description Default Value Type
para.s3.bucket
The S3 bucket where files will be stored by FileStore implementations.
org.paraio.us-east-1 String
para.s3.max_filesize_mb
Maximum file size for files uploaded to S3, in megabytes.
10 Integer
para.localstorage.folder
The local folder for file storage, when LocalFileStore is used.
String
para.localstorage.max_filesize_mb
Maximum file size for files stored locally, in megabytes.
10 Integer

Para Client

Property key & Description Default Value Type
para.client.ssl_protocols
SSL protocols allowed for a successul connection.
TLSv1.3 String
para.client.ssl_keystore
The SSL key store location. This contains the certificates used by the Para client.
String
para.client.ssl_keystore_password
The SSL key store password.
String
para.client.ssl_truststore
The SSL trust store location. This contains the certificates and CAs which the client trusts.
String
para.user_agent_id_enabled
Enable/disable User-Agent header in Para client.
true Boolean
Property key & Description Default Value Type
para.es.flavor
Eleasticsearch flavor - either elasticsearch or opensearch.
elasticsearch String
para.es.shards
The number of shards per index. Used when creating the root app index.
2 Integer
para.es.shards_for_child_apps
The number of shards per index for a child apps.
1 Integer
para.es.replicas
The number of copies to store of the root index.
0 Integer
para.es.replicas_for_child_apps
The number of copies to store of each child app index.
0 Integer
para.es.use_nested_custom_fields
Switches between normal indexing and indexing with nested key/value objects for custom properties. When this is false (normal mode), Para objects will be indexed without modification but this could lead to a field mapping explosion and crash the ES cluster.
false Boolean
para.es.async_enabled
Enable/disable asynchronous operations when indexing/unindexing.
false Boolean
para.es.bulk.flush_immediately
Eanble/disable immediately flushing the requests in BulkProcessor, concurrently (in another thread).
true Boolean
para.es.restclient_scheme
The scheme to use when connecting to the Elasticsearch server - http or https.
http String
para.es.restclient_host
The ES server hostname.
localhost String
para.es.restclient_port
The ES server port number.
9200 Integer
para.es.sign_requests_to_aws
Enable/disable request signing using the AWS V4 algorithm. For use with Amazon OpenSearch.
false Boolean
para.es.restclient_context_path
The context path where ES is deployed, if any.
String
para.es.auto_expand_replicas
Automatically make a replica copy of the index to the number of nodes specified.
0-1 String
para.es.root_index_sharing_enabled
Enable/disable root index sharing by child apps configured with isSharingIndex = true.
false Boolean
para.es.track_total_hits
Makes ES track the actual number of hits, even if they are more than the 10000.
true Boolean
para.es.aws_region
The AWS region where ES is deployed. Used for calculating request signatures.
eu-west-1 String
para.es.basic_auth_login
The username to use for authentication with ES.
String
para.es.basic_auth_password
The password to use for authentication with ES.
String
para.es.bulk.size_limit_mb
BulkProcessor flush threshold, in megabytes.
5 Integer
para.es.bulk.action_limit
BulkProcessor flush threshold in terms of batch size.
1000 Integer
para.es.bulk.concurrent_requests
BulkProcessor number of concurrent requests (0 means synchronous execution).
1 Integer
para.es.bulk.flush_interval_ms
BulkProcessor flush interval, in milliseconds.
5000 Integer
para.es.bulk.backoff_initial_delay_ms
BulkProcessor inital backoff delay, in milliseconds.
50 Integer
para.es.bulk.max_num_retries
BulkProcessor number of retries.
8 Integer
para.es.proxy_enabled
Enable/disable the Elasticsearch proxy endpoint.
false Boolean
para.es.proxy_path
The path to the ES proxy endpoint.
_elasticsearch String
para.es.proxy_reindexing_enabled
Enable/disable rebuilding indices through the Elasticsearch proxy endpoint.
false Boolean
Property key & Description Default Value Type
para.lucene.dir
The data folder where Lucene stores its indexes.
./ String

MongoDB DAO

Property key & Description Default Value Type
para.mongodb.uri
The MongoDB URI string - verrides host, port, user and password if set.
String
para.mongodb.database
The database name that Para will use. The database should exist before starting Para.
para String
para.mongodb.host
The hostname of the MongoDB server.
localhost String
para.mongodb.port
The MongoDB server port.
27017 Integer
para.mongodb.user
The username with access to the MongoDB database.
String
para.mongodb.password
The MongoDB user’s password.
String
para.mongodb.ssl_enabled
Enable/disable the SSL/TLS transport layer.
false Boolean
para.mongodb.ssl_allow_all
Allows a connection to any host over SSL by ignoring the certificate validation.
false Boolean

SQL DAO

Property key & Description Default Value Type
para.db.hostname
The hostname of the H2 server. Setting this will enable H2’s “server mode” and start a TCP server.
String
para.db.dir
The data directory for storing H2 databases.
./data String
para.db.user
The username with access to the H2 database.
para String
para.db.tcpServer
Parameters for the H2 TCP server.
String
para.sql.url
The server URL to connect to, without the jdbc: prefix.
String
para.sql.driver
The fully-qualified class name for your SQL driver.
String
para.sql.user
The username with access to the database.
user String
para.sql.password
The database user’s password.
secret String

Cassandra DAO

Property key & Description Default Value Type
para.cassandra.hosts
Comma-separated Cassandra server hosts (contact points).
localhost String
para.cassandra.keyspace
The name of the Cassandra keyspace to use.
para String
para.cassandra.user
The Cassandra username with access to the database.
String
para.cassandra.password
The password for the Cassandra user.
String
para.cassandra.port
The Cassandra server port to connect to.
9042 Integer
para.cassandra.replication_factor
Replication factor for the Cassandra keyspace.
1 Integer
para.cassandra.ssl_enabled
Enable/disable the SSL/TLS transport in Cassandra client.
false Boolean
para.cassandra.ssl_protocols
The protocols allowed for successful connection to Cassandra cluster.
TLSv1.3 String
para.cassandra.ssl_keystore
Cassandra client key store, containing the certificates to use.
String
para.cassandra.ssl_keystore_password
Password for the Cassandra client key store.
String
para.cassandra.ssl_truststore
Cassandra client trust store, containing trusted certificates and CAs.
String
para.cassandra.ssl_truststore_password
Password for the Cassandra trust store.
String

AWS DynamoDB DAO

Property key & Description Default Value Type
para.dynamodb.sse_enabled
Enable/disable SSE (encryption-at-rest) using own KMS, instead of AWS-owned CMK for all newly created DynamoDB tables.
false Boolean
para.dynamodb.replica_regions
Toggles global table settings for the specified regions.
String
para.dynamodb.backups_enabled
Enable/disable point-in-time backups in DynamoDB.
false Boolean
para.dynamodb.provisioned_mode_enabled
Enable/disable provisioned billing as an alternative to on-demand billing in DynamoDB.
false Boolean
para.dynamodb.max_read_capacity
The maximum read capacity when creating a table with provisioned mode enabled.
10 Integer
para.dynamodb.max_write_capacity
The maximum write capacity when creating a table with provisioned mode enabled.
Integer

Caffeine Cache

Property key & Description Default Value Type
para.caffeine.evict_after_minutes
Cache eviction policy - objects are evicted from Caffeine cache after this time.
10 Integer
para.caffeine.cache_size
Maximum size for the Caffeine cache map.
10000 Integer

Hazelcast Cache

Property key & Description Default Value Type
para.hc.async_enabled
Enable/disable asynchronous operations in the Hazelcast client.
true Boolean
para.hc.ttl_seconds
Time-to-live value (how long the objects stay cached) for cached objects, in seconds.
3600 Integer
para.hc.ec2_discovery_enabled
Enable/disable EC2 auto-discovery feature when deploying to AWS.
true Boolean
para.hc.aws_access_key
The AWS access key to use if EC2 auto-discovery is enabled in Hazelcast.
String
para.hc.aws_secret_key
The AWS secret key to use if EC2 auto-discovery is enabled in Hazelcast.
String
para.hc.aws_region
The AWS region to use if EC2 auto-discovery is enabled in Hazelcast.
String
para.hc.discovery_group
EC2 security group for cloud discovery of Hazelcast nodes.
hazelcast String
para.hc.max_size
Maximum number of objects to keep in Hazelcast cache.
5000 Integer
para.hc.eviction_policy
Hazelcast cache eviction policy - LRU or LFU.
LRU String

Miscellaneous

Property key & Description Default Value Type
para.max_items_per_page
Maximum results per page - limits the number of items to show in search results.
30 Integer
para.max_pages
Pagination limit - sets the highest page number possible.
1000 Integer
para.max_page_limit
Pagination limit - sets the maximum value for the limit request parameter, when it is used.
256 Integer
para.access_log_enabled
Enable/disable the Para access log.
true Boolean
para.shared_table_name
The name of the shared database table, used by shared apps.
0 String
para.fail_on_write_errors
Enable/disable throwing an exception when a write operation fails with errors.
true Boolean
para.import_batch_size
The maximum number of objects to import, in each batch, when restoring data from backup.
100 Integer
para.gzip_enabled
Enable/disable the GZIP filter for compressing API response entities.
false Boolean
para.debug_request_signatures
Enable/disable debuging info for each AWS V4 request signature.
false Boolean
para.vote_expires_after
Vote expiration timeout, in seconds. Users can vote again on the same content after this period has elapsed. Default is 30 days.
2592000 Integer
para.vote_locked_after
Vote locking period, in seconds. Vote cannot be changed after this period has elapsed. Default is 30 sec.
30 Integer
para.plugin_folder
The folder from which Para will load its JAR plugin files.
lib/ String
para.prepend_shared_appids_with_space
For internal use only! Prepends appid fields with a space for all shared apps.
false Boolean
para.print_version
Enable/disable version number printing in Para logs.
true Boolean
para.print_logo
Enable/disable printing the Para ASCII logo on startup.
true Boolean
para.markdown_soft_break
Sets the Markdown soft break character.
<br> String
para.markdown_allowed_follow_domains
A whitelist of domains, links to which will be allowed to be followed by web crawlers (comma-separated list).
String
para.aws_ses_region
AWS region to use in the AWSEmailer implementation.
eu-west-1 String
para.pidfile_enabled
Enable/disable PID file generation on startup.
true Boolean
para.default_separator
String separator - default is colon :.
: String
para.default_encoding
Default character encoding - UTF-8.
UTF-8 String

All configuration properties can be overridden using system properties, e.g. System.setProperty("para.port", "8081"), or -Dpara.port=8081 or using ENV variables export para_port=8081.

Para uses the excellent Config library by TypeSafe. For more information about how it works, see the README for it.

Modules

In the para-server package, there are several core modules:

  • PersistenceModule - defines the core persistence class implementation
  • SearchModule - defines the core search class implementation
  • CacheModule - defines the core cache class implementation
  • EmailModule - defines an email service implementation for sending emails
  • AOPModule - manages the indexing and caching aspects of Para objects
  • QueueModule - defines a queue service implementation
  • StorageModule - defines a file storage service, e.g. Amazon S3

You can override all of the above using the ServiceLoader mechanism in Java like so:

  1. Create a file com.google.inject.Module inside the META-INF/services folder
  2. Then add the full class names of all your modules, one on each line
  3. Para will load these modules on startup use them instead of the default ones

For example, let’s say you want to implement a new PersistenceModule which connects to MySQL. You can define it like so:

class MySqlModule extends AbstractModule {
    protected void configure() {
        bind(DAO.class).to(MySqlDAO.class).asEagerSingleton();
    }
}

Then implement the DAO interface in your class MySqlDAO so that it connects to a MySQL server and stores and loads objects. Finally write the full class name com.company.MySqlModule to the file com.google.inject.Module and Para will use this module as its default persistence module.

You can also override modules programmatically like this:

ParaServer.initialize(Modules.override(ParaServer.getCoreModules()).with(new Module() {
    public void configure(Binder binder) {
        binder.bind(DAO.class).to(MyDAO.class).asEagerSingleton();
        binder.bind(Cache.class).to(MyCache.class).asEagerSingleton();
        binder.bind(Search.class).to(MySearch.class).asEagerSingleton();
    }
}));

Also, you could start Para with the default modules like this:

// Initialize Para and call each listener.onInitialize()
ParaServer.initialize();

// Finally, destroy all resources by calling each listener.onDestroy()
ParaServer.destroy();

If you’re defining your own custom classes, don’t forget to set:

System.setProperty("para.core_package_name", "com.company.myapp.core");

There are two additional methods Para.initialize() and Para.destroy() which are now part of para-core. The difference between these and the ones above is:

  • Para.initialize() - invokes all registered InitializeListener instances and prints out logo to console;

  • ParaServer.initialize() - loads all Guice modules, binds them and injects concrete types into each InitializeListener, then calls Para.initialize();

  • Para.destroy() - invokes all registered DestroyListener instances and prints out a log message;

  • ParaServer.destroy() - injects concrete types into each DestroyListener and calls Para.destroy().

Para uses Google Guice as its module manager and DI system.

Environment

para.env is an important configuration property that turns features on and off. There are three main values for it - embedded, development, production, but you can define more.

When you are in embedded mode Para will use H2 as a database and pure Lucene as a search engine. This is the simplest way to get started with Para as you don’t need to configure anything.

Important: Embedded mode is recommended for local development only.

When you are in development mode Para will try to connect to a DynamoDB server running on localhost. It will also try to connect to a local Elasticsearch server. This mode was created so that you can keep your DB and search servers running in the background while developing your application rather than loading them on each deploy. This will make redeploys a bit faster.

Finally when you are ready to deploy your app to a real server set para.env to production. This will enable object caching and will try to connect to the real AWS DynamoDB service. Also you have to set up a new node for Elasticsearch or run it locally.

para.env

DAO

Cache

Search

embedded (default)

H2DAO

off

on

development

AWSDynamoDAO

off

on

production

AWSDynamoDAO

on

on

Plugins

Para will look for various plugins like IOListeners, CustomResourceHandlers, DAOs etc, on startup. The folder in which plugins JARs should be placed is ./lib/, by default, but it can be configured like so:

para.plugin_folder = "plugins/"

DAO, Search and Cache plugins

In version 1.18 we expanded the support for plugins. Para can now load third-party plugins for various DAO implementations, like MongoDB for example. The plugin para-dao-mongodb is loaded using the ServiceLoader mechanism from the classpath and replaces the default AWSDynamoDAO implementation.

To create a plugin you have to create a new project and import para-core with Maven and extend one of the three interfaces - DAO, Search, Cache. Implement one of these interfaces and name your project by following the convention:

  • para-dao-mydao for DAO plugins,
  • para-search-mysearch for Search plugins,
  • para-cache-mycache for Cache plugins.

For example, the plugin for MongoDB is called para-dao-mongodb and implements the DAO interface with the MongoDB driver for Java.

You also need to create one file inside src/main/resources/META-INF/services/ in your plugin project:

  • com.erudika.para.persistence.DAO for DAO plugins,
  • com.erudika.para.search.Search for Search plugins,
  • com.erudika.para.cache.Cache for Cache plugins.

Inside this file you put the full class name of your implementation, for example com.erudika.para.persistence.MyDAO, on one line and save the file.

To load a plugin follow these steps:

  1. Place the plugin JAR in a folder called lib (depends on configuration) or WEB-INF/lib in the same folder as the Para server or include the plugin through Maven,
  2. Set the configuration property para.dao = "MyDAO" or para.search = "MySearch" or para.cache = "MyCache" (use the simple class name here)
  3. Start the Para server and the new plugin should be loaded

Important: Each plugin must implement and register a DestroyListener on initialization where all resources (connections, streams, pools) should be properly released and closed. Using Runtime.getRuntime().addShutdownHook() is not recommended because in some cases that method is not executed.

Custom event listeners

To register your own InitializeListeners and DestroyListeners add the following code to one of your constructors or inside a static block in one of your classes:

// this has to be registered before Para.initialize() is called
Para.addInitListener(new InitializeListener() {
    public void onInitialize() {
        // TODO: do stuff on initialization...
    }
});

Para.addDestroyListener(new DestroyListener() {
    public void onDestroy() {
        // TODO: release resources...
    }
});

Custom I/O listeners

An I/O listener is a callback function which is executed after an input/output (CRUD) operation. After a call is made to one of the DAO methods like read(), update(), etc., all registered listeners are notified and called. It is recommended that the code inside these listeners is asynchronous or less CPU intensive so it does not slow down the calls to DAO.

Para.addIOListener(new IOListener() {
    public void onPreInvoke(Method method, Object[] args) {
        // do something before the CRUD operation...
    }
    public void onPostInvoke(Method method, Object result) {
        // do something with the result...
    }
});

Custom listeners for app events

These listeners can be registered to execute code when an app is created or deleted. This is useful when we need to do additional operations like creating DB tables and/or creating indexes for the new app. Also we might want to clean up those after the app is deleted. Example:

App.addAppCreatedListener(new AppCreatedListener() {
    public void onAppCreated(App app) {
        if (app != null) {
            createTable(app.getAppIdentifier());
        }
    }
});

App.addAppDeletedListener(new AppDeletedListener() {
    public void onAppDeleted(App app) {
        if (app != null) {
            deleteTable(app.getAppIdentifier());
        }
    }
});

Additionally, you can listen for changes to the custom app settings with these two event listeners:

App.addAppSettingAddedListener(new AppSettingAddedListener() {
    public void onSettingAdded(App app, String settingKey, Object settingValue) {
        if (app != null) {
            // trigger something
        }
    }
});

App.addAppSettingRemovedListener(new AppSettingRemovedListener() {
    public void onSettingRemoved(App app, String settingKey) {
        if (app != null) {
            // trigger something else
        }
    }
});

Custom context initializers

Para will automatically pick up your classes which extend the Para class. They should be annotated with @Configuration, @EnableAutoConfiguration and @ComponentScan. The Para class implements Spring Boot’s WebApplicationInitializer which creates the root application context.

In your custom initializers you have full access to the ServletContext and this is a good place to register your own filters and servlets. These initializer classes also act as an alternative to web.xml by providing programmatic configuration capabilities.

Custom API resource handlers

Since version 1.7, you can register custom API resources by implementing the CustomResourceHandler interface. Use the ServiceLoader mechanism to tell Para to load your handlers - add them to a file named:

com.erudika.para.rest.CustomResourceHandler

in META-INF/services where each line contains the full class name of your custom resource handler class. On startup Para will load these and register them as API resource handlers.

The CustomResourceHandler interface is simple:

public interface CustomResourceHandler {
    // the path of the resource, e.g. "my-resource"
    String getRelativePath();
    // handle GET requests
    Response handleGet(ContainerRequestContext ctx);
    // handle PUT requests
    Response handlePut(ContainerRequestContext ctx);
    // handle POST requests
    Response handlePost(ContainerRequestContext ctx);
    // handle DELETE requests
    Response handleDelete(ContainerRequestContext ctx);
}

You can use @Inject in your custom handlers to inject any object managed by Para.

Core classes

In the package com.erudika.para.core there are a number of core domain classes that are used throughout Para. These are common classes like User and App which you can use in you application directly or extend them. You can use them for your objects but you’re always free to create your own. To do that, simply POST a new object with type: mytype through the API and your new type will be automatically registered.

Note: Type definitions cannot contain the symbols / and #.

class description

User

Defines a basic user with a name, email, password. Used for user registration and security.

Address

Defines an address with optional geographical coordinates. Used for location based searching.

App

Defines an application within Para. Usually there’s only one existing (root) app.

Tag

Simple tag class which contains a tag and its frequency count. Basically any Para object can be tagged. Used for searching.

Vote

Defines a user vote - negative or positive. Useful for modeling objects which can be voted on (likes, +1s, favs, etc).

Translation

Holds a translated string. Can be used for collecting translations from users.

Sysprop

System class used as a general-purpose data container. It’s basically a map.

Linker

System class used for implementing a many-to-many relationship between objects.

Webhook

System class used for storing webhook metadata.

Voting

Para implements a simple voting mechanism for objects - each object can be voted up and down an has a votes property. Voting is useful for many application which require sorting by user votes. When a vote is cast, a new object of type Vote is created to store the vote in the database. Users have a configurable time window to amend their vote, they can no longer vote on that particular object.

Serialization

Para handles serialization and deserialization using a combination of Jackson, BeanUtils and Java reflection. The process happens in the ParaObjectUtils class, and more precisely inside two particular methods - setAnnotatedFields() and getAnnotatedFields().

When defining your custom classes and fields, you’re free to use any Java type, primitive or your own custom classes. Basically, any type compatible with Jackson will work with Para as well. You can also use Jackson annotations on your fields. A common example is declaring custom serializers and deserializers:

public class MyCustomClass extends Sysprop {
    @JsonSerialize(using = CatSerializer.class)
    @JsonDeserialize(using = CatDeserializer.class)
    @Stored private Cat cat;
}

Map<String, Object> properties = ParaObjectUtils.getAnnotatedFields(customClass, false);
MyCustomClass deserialized = ParaObjectUtils.setAnnotatedFields(properties);

Notice the false at the end of getAnnotatedFields(). It means that whenever a complex/custom type is encountered, it will not be flattened to a JSON string. This allows you to persist objects in their original format or flatten them (true) to JSON. This is really useful in some situations.

Once a ParaObject is serialized to a Map, each DAO decides how it will persist that data. Some DAO implementations persist objects in two columns - id and json, containing the JSON representation of that Map. Others, like MongoDBDAO, preserve the format of objects and they are stored as JSON documents (not a JSON string). When you’re implementing your own DAO classes, it’s entirely up to you to decide how ParaObjects will be persisted. There are absolutely no restrictions for this, as long as the data comes back as a Map at the point of retrieval.

It’s important to mention that serialization/deserialization can happen in both the server and the client. The Para clients will read your custom classes and serialize them to JSON before sending them to the Para server. When you define your custom classes on the clientside, the server doesn’t know about them - they are simply converted to Sysprop (a container class) objects when received from the client. This, however, is only done for practical reasons on the serverside. It does not mean that on the clientside, the objects returned from the server will be deserialized to Sysprop, on the contrary, they will be deserialized to your custom classes.

Annotations

Para uses annotations to mark what attributes of a class need to be saved. There are several annotations that Para uses but the two main ones are @Stored and @Locked.

@Stored tell Para that an attribute needs to be persisted but that field should not be declared as transient otherwise it will be skipped.

@Locked is used as a filter for those attributes that are read-only like type and id. These are created once and never change so when calling update() they will be skipped.

Note: All of the annotations above are ignored on the clientside and not supported by ParaClient, yet. By default all fields declared on the clientside or through the API will be stored and not locked, i.e. they can be updated.

User-defined classes

Let’s say you have a class Article in your application that you wish to persist. You first implement the ParaObject interface, then add a few data fields to it. Implementing the interface is trivial as the basic functionality of the required methods is already implemented in the CoreUtils.

You need to specify which fields you want to be saved by adding the @Stored annotation. Additionally, you can add the @Locked annotation to prevent a field from being updated (i.e. it can only be set on create()). This is useful for fields that contain sensitive data, which should not be modified easily.

class Article implements ParaObject {
    @Stored private String title;
    @Stored private String text;
    @Stored private Map<?, ?> someCustomProperty;
}

When defining your custom properties (fields) you can either declare they as Java types like List, Map, String, boolean, or use your own custom types.

You don’t have to define common fields like id or name because they are already defined in the parent class. Now you can create a new article like so:

System.setProperty("para.core_package_name", "com.erudika.para.core");

Article a = new Article();
a.setTitle("Some title");
a.setText("text...");
// the article is saved and a new id is generated
String id = a.create();

Important: Don’t forget to set para.core_package_name to point to the package where your ParaObject classes are. Para will scan that package and will use those new definitions to serialize and deserialize objects.

Updating and deleting is easy too:

a.setTitle("A new title");
a.update();
// or
a.delete();

When you call create() Para will intercept that call and automatically index and cache the object. Calling the methods update() and delete() also get intercepted and will update or delete that object in/from the search index and cache respectively.

If you want to read an object, first you have to get access to the DAO object. You can either:

  • call Para.getDAO()
  • get it by calling CoreUtils.getInstance().getDao()
  • @Inject it with Para.injectInto()

Then you can read an object using its id:

// returns a single article or null
Article readA = dao.read(id);

You can also define custom types of objects through the REST API by changing the type property on your objects. Keep in mind that the following are reserved words and they should not be used for naming your types (plural form included): “search(es)”, “util(s)”.

Note that you can create objects with custom types and fields through the REST API without having to define them as Java classes like above. These custom objects will be based on the generic Sysprop class but can have any number of custom properties (fields). When doing search on custom fields, add the “properties” prefix to them, like properties.myfield.

For example, lets create another custom Article object through the API. We’ll add to it a custom field called author:

POST /v1/articles

{
 "appid": "myapp",
 "author": "Gordon Freeman"
}

This creates a new Article and indexes all fields including the custom field author. To search for objects through the API, containing the author field we can do a request like this:

GET /v1/articles/search?q=properties.author:Gordon*

Note that we have q=properties.author:... instead of q=author:.... This is due to the fact that custom fields are stored in Para using a nested Map called properties (see the Sysprop class).

Apps

Apps allow you to have separate namespaces and data models for different purposes. Each app lives in its own separate database table and is independent from apps.

Each app has a unique identifier, like “my-custom-app”. When an object is created, Para will attach the app identifier to it automatically. Apps also have a set of data types, as set of permissions and validation constraints. Data types can be created on-the-fly, for example you can create a type called “article” and it will have be available as a new API resource at /v1/article (and in plural form /v1/articles).

Creating and deleting apps

Initially Para creates a default root app with an id equal to the value of the para.app_name configuration parameter. If you need to have only one app then you don’t need to do anything. If you want to create multiple apps then you must call Para.newApp() or make an authenticated request to the API GET /v1/_setup/{app_name}. This is a special API request and it must be signed with the keys of the root app. Alternatively, download the Para CLI and run:

$ para-cli setup
$ para-cli new-app "my-app" --name "My new app"

Every app can delete itself through the API with DELETE /v1/app/myapp. Another way to do this is by programmatically calling app.delete() from your code.

Currently Para organizes objects in one table per app and uses a single shared search index unless that app has sharingIndex set to false. If this is the case then a separate search index is created for that app. It is possible to make Para use a single database table for all apps by prefixing id fields (e.g. app1_id1: {data}) but this is not yet implemented.

You can also set custom settings for each app through the settings API /v1/_settings using GET, PUT and DELETE. These can be OAuth credentials for social apps or other configuration details that are specific for the app.

Social sign-in for apps

Apps can have their own separate credentials for social sign-in, like an OAuth app_id and secret. These are stored within the app object and can be used to create/login users to the app that has these credentials. For example, we can send a request to /facebook_auth?appid=myapp where myapp is not the root app. This will tell Para to look for Facebook keys inside that app. This allows for each Para app to have a corresponding Facebook app (or any other app on the supported social networks, see JWT sign in).

Additionally, we’ve added two custom settings which can tell Para where to redirect users after a successful login or a login failure. These are signin_success and signin_failure (see Custom Settings). Here’s an example of all settings combined:

{
    "fb_app_id": "123U3VTNifLPqnZ1W2",
    "fb_secret": "YXBwOnBhcmE11234151667",
    "signin_success": "/dashboard",
    "signin_failure": "/signin?error"
}

Note that these settings will work for the traditional authentication flow through the browser and the standard endpoints /facebook_auth?appid=myapp, /github_auth?appid=myapp, /google_auth?appid=myapp, and the rest. The other way of authenticating users is through the JWT sign in API which requires an OAuth access token and doesn’t require stored OAuth credentials - these are not even checked because we are supplied with a ready-to-use token.

Resource permissions

When creating new users, we usually want to specify which resources they can access. This is why we added a few methods for adding and removing resource permissions. Each app can have any number of users and each user can have a set of permissions for a given resource. Resources are identified by name, for example the _batch resource would represent requests going to /v1/_batch.

There are several methods and flags which control which requests can go through. These are:

  • GET, POST, PUT, PATCH, DELETE - use these to allow a certain method explicitly
  • ? - use this to enable public (unauthenticated) access to a resource
  • - - use this to deny all access to a resource
  • * - wildcard, allow all request to go through
  • OWN - allow subject to only access objects they created

Let’s look at a few example scenarios where we give users permission to access the _batch. We have two users - user one with id = 1 and user two with id = 2. We’ll use the following methods:

boolean grantResourcePermission(String subjectid, String resourcePath, EnumSet<AllowedMethods> permission);
// when not using the Java client - permission is an array of HTTP methods
boolean grantResourcePermission(String subjectid, String resourcePath, String[] permission);
boolean revokeResourcePermission(String subjectid, String resourcePath);

These methods allow the use of wildcards * for subjectid and resourcePath arguments.

Scenario 1: Give all users permission to READ - this allows them to make GET requests:

app.grantResourcePermission("*", "_batch", AllowedMethods.READ);

Scenario 2: Give user 1 WRITE permissions - allow HTTP methods POST, PUT, PATCH, DELETE:

app.grantResourcePermission("1", "_batch", AllowedMethods.WRITE);

Also you could grant permissions on specific objects like so:

paraClient.grantResourcePermission("user1", "posts/123", AllowedMethods.DELETE);

This will allow user1 to delete only the post object with an id of 123.

Scenario 3: Give user 2 permission ot only make POST requests:

app.grantResourcePermission("2", "_batch", AllowedMethods.POST);

Note that all users still have the READ permissions because permissions are compounded. However, when grantResourcePermission() is called again on the same subject and resource, the new permission will overwrite the old one.

Scenario 4: Revoke all permissions for user 1 except READ:

app.revokeAllResourcePermissions("1");

Scenario 5: Grant full access or deny all access to everyone:

app.grantResourcePermission("*", "*", AllowedMethods.ALL);
app.grantResourcePermission("*", "*", AllowedMethods.NONE);

Scenario 6: Grant full access to user 1 but only to the objects he/she created. In this case user 1 will be able to create, edit, delete and search todo objects but only those which he/she created, i.e. creatorid == 1.

app.grantResourcePermission("1", "todo", ["*", "OWN"]);
app.grantResourcePermission("1", "todo/*", ["*", "OWN"]);

To get all permissions for both users call:

app.getAllResourcePermissions("1", "2");

The default initial policy for all apps is “deny all” which means that new users won’t be able to access any resources, except their own object and child objects, unless given explicit permission to do so.

You can also create “anonymous” permissions to allow unauthenticated users to access certain resources:

app.grantResourcePermission("*", "public/resource", AllowedMethods.READ, true);

This resource is now public but to access it you still need to specify your access key as a parameter:

GET /v1/public/resource?accessKey=app:myapp

Alternatively, on the client-side, you can set the Authorization header to indicate that the request is anonymous:

Authorization: Anonymous app:myapp

The special permission method ? means that anyone can do a GET request on public/resource.

To check if user 1 is allowed to access a particular resource call:

// returns 'false'
app.isAllowedTo("1", "admin", "GET");

Validations

Para supports JSR-303 validation annotations and also allows you to define validation constraints in two different ways. One way is to attach annotations to fields in Java classes. The other way is by using the constraints API to add validation constraints to any object, be it core or user-defined. This method is more flexible as it allows you to validate any property of any object.

The built-in constraints are:

required, min, max, size, email, digits, pattern, false, true, future, past, url.

Note: Objects are validated on create() and update() operations only.

Annotation-based validation constraints

You can use any of the JSR 303 annotations specified by the javax.validation.constraints package, part of Java EE. Example:

@Stored @NotBlank @Email
private String email;

This will tell Para to validate that field and check that it actually contains an email. If the validation fails, the object containing that field will not be created or updated.

User-defined validation constraints

Annotations work fine for most objects but are less useful when we want to define objects through the API. For this purpose we can use the constraints API:

boolean addValidationConstraint(String type, String field, Constraint c);

boolean removeValidationConstraint(String type, String field, String constraintName);

To create a constraint you can use the static methods provided by the Constraint class. For example calling Constraint.email() will return a new constraint object for checking email addresses.

We can use these methods to define constraints on custom types and fields that are not yet defined or are created by the API.

To manually validate an object you can use:

String[] errors = ValidationUtils.validateObject(App app, ParaObject po);

Or through ParaClient:

// return a JSON object with all validation constraints
paraClient.validationConstraints();

The returned string array contains 0 elements if the po is valid or a list of errors that were encountered on validation.

Integration with the client-side

You can easily implement client-side validation by getting the JSON object containing all validation constraints for all Para classes.

String jsonValidations = ValidationUtils.getAllValidationConstraints(App app);

This returned JSON is in the following format (note that type names are all in lowercase):

'user': {
    'email': {
        'email': {
            'message': 'messages.email'
        },
        'required': {
            'message': 'messages.required'
        }
    },
    'identifier': {
        'required': {
            'message': 'messages.required'
        }
    }
    ...
},
...

This format used by the excellent JavaScript validation tool Valdr but can easily be integrated with other client-side validators.

Para uses Hibernate Validator for data validation.

Webhooks

Webhook were implemented in v1.32.0. The implementation is fully asynchronous and uses a queue for decoupling the message publishing from the actual processing and delivery of payloads. There’s a LocalQueue implementation which holds messages in memory and a LocalRiver (worker) which periodically pulls messages from the queue and forwards them to their destinations. The AWSQueue class implements webhooks processing based on AWS SQS queues and is recommended for production use. Any other queue server can be potentially supported via a plugin which implements the Queue interface.

To enable the webhooks functionality in Para, add para.webhooks_enabled = true in your application.conf.

The Webhook class is used for storing webhook metadata. Each Webhook object represents a destination which will receive a POST request with a certain payload. The payload is determined by the type of event to which that webhook is subscribed to. For example, a webhook might be a subscribed to all update events in Para, and also it might only be interested in updated user objects. So we can register a new webhook like so:

POST /v1/webhooks
{
    "urlEncoded": true,
    "update": true,
    "targetUrl": "https://destination.url",
    "secret": "secret",
    "typeFilter":"user"
}

The urlEncoded parameter sets the Content-Type header for the payload. By default that’s application/x-www-form-urlencoded, but if urlEncoded is false, the content type will be application/json.

There are 6 event types: update, create, delete, updateAll, createAll, deleteAll. You can also register webhooks which are subscribed to all events on all types in Para:

POST /v1/webhooks
{
    "active": true,
    "urlEncoded": true,
    "update": true,
    "create": true,
    "delete": true,
    "updateAll": true,
    "createAll": true,
    "deleteAll": true,
    "targetUrl": "https://destination.url",
    "secret": "secret",
    "typeFilter":"*"
}

If the typeFilter is either blank or *, all selected events will be sent to the destination, regardless of the object type.

If the webhook’s response code is not 2xx, it is considered as failed. Webhooks with too many failed deliveries (10 by default) will be disabled automatically. The number of maximum failed attemts can be adjusted like so:

para.max_failed_webhook_attempts = 15

Events are intercepted by the WebhookIOListener which listens for mutating DAO methods and after each method invocation sends a message to the queue for all registered Webhook objects. Each message is signed with HmacSHA256 which is sent to the destination in a X-Webhook-Signature header. The signature takes the webhook secret and uses it to sign only the payload JSON string (whitespace removed). The worker node (might be the same machine) then pulls messages from the queue and sends them to their destinations using a POST request. Here’s an example POST request:

POST /test HTTP/1.1
Host: parawebhooks.requestcatcher.com
Accept-Encoding: gzip,deflate
Connection: Keep-Alive
Content-Length: 327
Content-Type: application/json
User-Agent: Para Webhook Dispacher 1.32.0
X-Para-Event: update
X-Webhook-Signature: FFEma4/Uc5gGCs974cJNa873kzsumFgqPGVIGOEexmY=

{"appid":"scoold","event":"update","items":[{"id":"tag:acpi","timestamp":1486848081865,"type":"tag","appid":"scoold","updated":1561644547996,"name":"ParaObject tag:acpi","votes":2,"version":0,"stored":true,"indexed":true,"cached":true,"tag":"acpi","count":1,"objectURI":"/tags/acpi","plural":"tags"}],"timestamp":1561644548430}

The format of the message is this:

{
    "appid": "myapp",
    "event": "create",
    "timestamp": 1486848081865,
    "items": [{}, ...]
}

The receiving party should verify the signature of each payload by computing Base64(HmacSHA256(payload, secret)).

Custom events

In addition to the standard create, update, delete events you can create webhooks which are subscribed to custom events. For example you can have a webhook which is subscribed to events like post.like or user.mention. These events are triggered from the code of your application using the Para API. Let’s say we have a post which is liked by someone - the code which handles the like event will notify Para that post.like has occured along with a custom payload, in this case the post object and the object of the user who liked the post. Para will then dispatch that payload to the appropriate target URLs (subscribers). Custom events allow you to create applications which follow the best practices of RESTHooks and this makes it easy to integrate them with other applications (see Zapier). Here’s an example request which would trigger a custom event post.like via the API:

POST /v1/webhooks
{
    "triggeredEvent": "post.like",
    "customPayload": {
        "post_id": "5129509320",
        "title": "Hello world",
        "liked_by": {
            "user_id": "581703234",
            "name": "Gordon"
        }
    }
}

The response object returned from this request should be ignored.

DAO interface

The DAO interface declares the following methods:

method description

String create(P obj)

Persists an object. A new id should be generated if absent.

P read(String key)

Reads an object from the data store for a given id. Returns null if the object for this id is missing.

void update(P obj)

Updates an object. Implementations of this method should set a timestamp to the field updated.

void delete(P obj)

Deletes an object completely (the opposite of create).

List<P> readAll(List<String> keys, boolean all)

Batch operation for read(). If all is false, only the id and type columns should be returned.

void createAll(List<P> objects)

Batch operation for create().

void updateAll(List<P> objects)

Batch operation for update().

void deleteAll(List<P> objects)

Batch operation for delete().

Optimistic locking

Each DAO implementation is responsible for supporting optimistic locking if the underlying database is capable of performing conditional write operations natively. The ParaObject interface contains a version field which can be used to store information about the current version of each object and it’s value should be incremented by the database, if an update operation succeeds. The locking mechanism is simple - the database checks if the supplied object version matches the one stored in the database. If the two versions match, the update operation is executed, otherwise it fails. A version value of -1 indicates a failed update and it’s the responsibility of the API clients to handle such failures. If the version value is positive, the update has been successful, and 0 means the field is unused or the object has not been persisted yet.

Optimistic locking is enabled for each ParaObject individually by setting its version field to a positive value different than 0. To disable, set the version field back to 0.

A DAO object only deals with storing objects in a database. It is not concerned with relationships between objects or constraint validation or any other operations like caching, indexing, etc.

Data consistency and resilience

Para is designed to store data in more than one place - a database and a search index. Both the database and index may be configured to have multiple replicas of the same data to ensure resilience to data loss. If the database loses its data, we could still recover it from the search index with para.read_from_index = true. If the index is corrupted, we can simply rebuild it with the request POST /v1/_reindex.

Para makes an effort to keep the database and index in sync at all times but there might be occasional failures in either of the two layers and we end up with inconsistent data. So when an object is indexed but does not appear in the database, Para will print out a warning log message. If an object is persisted to DB but an error occurs during indexing, you have the option to set para.es.fail_on_indexing_errors (Elasticsearch only). This will cascade an exception back to the client and it can decide whether to revert the operation or try again. Additionally, if an update operation fails due to a version mismatch (see optimistic locking above), Para automatically prevents the indexing and caching of that object. In relation to this, para.fail_on_write_errors is true by default, which means that if there’s any error returned from the DB on write operations, all subsequent index operations will be skipped.

Cassandra

The Cassandra plugin adds support for Apache Cassandra. The class CassandraDAO is a DAO implementation and is responsible for connecting to a Cassandra cluster and storing/retrieving objects (items) to/from it. All operations are carried out using the latest Cassandra Java Driver by DataStax, compatible with Cassandra 3.x.

There are several configuration properties for Cassandra (these go in your application.conf file):

property description

para.cassandra.hosts

Comma-separated server hostnames (contact points). Defaults to localhost.

para.cassandra.port

The server port to connect to. Defaults to 9042.

para.cassandra.keyspace

The keyspace name that Para will use. Default is equal to para.app_name.

para.cassandra.user

The username with access to the database. Defaults to "".

para.cassandra.password

The password. Defaults to "".

para.cassandra.replication_factor

Replication factor for the keyspace. Defaults to 1.

para.cassandra.ssl_enabled

Enables the secure SSL/TLS transport. Defaults to false.

The plugin is on Maven Central. Here’s the Maven snippet to include in your pom.xml:

<dependency>
  <groupId>com.erudika</groupId>
  <artifactId>para-dao-cassandra</artifactId>
  <version>{version}</version>
</dependency>

Alternatively you can download the JAR from the “Releases” tab above put it in a lib folder alongside the server JAR file para-x.y.z.jar. Para will look for plugins inside lib and pick up the Cassandra plugin.

Finally, set the config property:

para.dao = "CassandraDAO"

This could be a Java system property or part of a application.conf file on the classpath. This tells Para to use the Cassandra Data Access Object (DAO) implementation instead of the default.

See Plugins for more information about how you can create your own plugins.

For more information about using Cassandra, see the official docs.

DynamoDB

Para can work with DynamoDB by using the AWSDynamoDAO implementation. That class is responsible for connecting to Amazon’s DynamoDB server and storing/retrieving objects (items) to/from it. All operations are carried out using the latest AWS Java SDK.

Note: DynamoDB doesn’t support batch update requests so AWSDynamoDAO does not batch update requests. It simply executes all update requests in a sequence.

The implementation adds a default prefix para- to DynamoDB tables, so if you have an app called “myapp” your table for that will be called para-myapp.

This DAO implementation supports optimistic locking through conditional update expressions in DynamoDB.

Server-side encryption (SSE, encryption-at-rest) is enabled by default for all tables. To switch between from AWS-owned CMK to using your account’s KMS, the following property:

para.dynamodb.sse_enabled = true

When this is true, your account’s KMS will be used for encrypting the data in the table, otherwise an AWS-owned CMK will be used. Defaults to false. Note: SSE will work for newly created tables only. Currently, you can’t enable encryption at rest on an existing table. After encryption at rest is enabled, it can’t be disabled.

Table sharing

In v1.21, we added new functionality to AWSDynamoDAO which enables apps to share the same table. This is useful for certain deployment scenarios where you have a large number of apps (and tables) which are rarely accessed and have low throughput. This makes it expensive to run Para on many DynamoDB tables which remain underutilized for long periods of time. So with table sharing you can reduce your DynamoDB bill to a minimum by having a shared table that contains all the objects for all those apps. You can enable this feature by setting the following in your config:

para.prepend_shared_appids_with_space = true

This will prepend the appid property with a space character for those apps that have isSharingTable set to true, thus letting the DAO know that this app belongs to the shared table, instead of sending its data to a separate table. The shared table name is controlled by para.shared_table_name and defaults to “0”.

The shared table keys are a combination of appid and id (e.g. myapp_679334060962615296). Para will also automatically create a global secondary index (GSI) on the shared table, with a primary key appid and secondary key timestamp, to facilitate queries like dao.readPage().

See Modules for more information about how you can override the default implementation.

For more information about DyanamoDB see the documentation on AWS.

MongoDB

Since v1.18.0 Para supports plugins and the the first official plugin adds support for MongoDB. The class MongoDBDAO is a DAO implementation and is responsible for connecting to a MongoDB server and storing/retrieving objects (items) to/from it. All operations are carried out using the latest MongoDB Java Driver compatible with MongoDB 3.2.

There are several configuration properties for MongoDB (these go in your application.conf file):

property description

para.mongodb.uri

The client URI string. See MongoClientURI. Overrides host, port, user and password if set. Defaults to blank.

para.mongodb.host

The hostname of the MongoDB server. Defaults to localhost.

para.mongodb.port

The server port to connect to. Defaults to 27017.

para.mongodb.database

The database name that Para will use. Default is equal to para.app_name.

para.mongodb.user

The username with access to the database. Defaults to blank.

para.mongodb.password

The password. Defaults to blank.

para.mongodb.ssl_enabled

Enables the secure SSL/TLS transport. Defaults to false.

para.mongodb.ssl_allow_all

Allows any hostname by skipping the certificate verification. Defaults to false.

The plugin is on Maven Central. Here’s the Maven snippet to include in your pom.xml:

<dependency>
  <groupId>com.erudika</groupId>
  <artifactId>para-dao-mongodb</artifactId>
  <version>{version}</version>
</dependency>

Alternatively you can download the JAR and put it in a lib folder alongside the server JAR file para-x.y.z.jar. Para will look for plugins inside lib and pick up the plugin.

Finally, set the config property:

para.dao = "MongoDBDAO"

This could be a Java system property or part of a application.conf file on the classpath. This tells Para to use the MongoDB Data Access Object (DAO) implementation instead of the default.

See Plugins for more information about how you can create your own plugins.

For more information about using MongoDB, see the official manual.

Generic SQL

Besides the default H2 implementation, there’s a generic SQL DAO plugin which adds support for all the major RDBMS - MySQL/MariaDB, PostgreSQL, Microsoft SQL Server, Oracle and SQLite. The class SqlDAO implements the DAO interface is responsible for connecting to an SQL server at a given URL. All database connections are managed by HikariCP - a lightweight, production-ready, JDBC connection pool.

Before you can use this plugin, you need to find an download the right driver for your database. This is a small package which must be loaded from the classpath along with the plugin and the HikariCP library. You also have to specify the class name of the loaded driver in the main Para configuration file. For SQLite the JDBC driver is bundled with the plugin so it just works.

There are several configuration properties for this plugin (these go in your application.conf file):

property description

para.sql.driver

The fully-qualified class name for your SQL driver. Defaults to null.

para.sql.url

The server URL to connect to, without the jdbc: prefix. Defaults to null.

para.sql.user

The username with access to the database. Defaults to user.

para.sql.password

The DB password. Defaults to secret.

The plugin is on Maven Central. Here’s the Maven snippet to include in your pom.xml:

<dependency>
  <groupId>com.erudika</groupId>
  <artifactId>para-dao-sql</artifactId>
  <version>{version}</version>
</dependency>

Alternatively you can download the JAR and put it in a lib folder alongside the server JAR file para-x.y.z.jar. Para will look for plugins inside lib and pick up the plugin.

Important: Except for SQLite, you must download the appropriate JDBC driver package for your database and put it in the lib/ folder.

Then you have to set the loader.path=lib system property in order to make the Spring Boot launcher add that folder to the classpath. For example:

java -jar -Dconfig.file=./application.conf -Dloader.path=lib  para.jar

Here’s an example configuration for this plugin (these go inside your application.conf):

para.sql.driver = "com.mysql.jdbc.Driver"
para.sql.url = "mysql://localhost:3306"
para.sql.user = "user"
para.sql.password = "secret"

Finally, set the config property:

para.dao = "SqlDAO"

This could be a Java system property or part of a application.conf file on the classpath. This tells Para to use the SQL DAO implementation instead of the default.

SQLite, for example, has the simplest configuration:

para.sql.driver = "org.sqlite.JDBC"
para.sql.url = "sqlite:/home/user/para.db"

The environment variable para.sql.url is required and provides the URL to connect to the SQL database. The SQL DAO uses JDBC and will prefix your URL with the JDBC protocol, so you don’t need to include the JDBC protocol in your URL path. For example, to connect to a MySQL server with URL mysql://localhost:3306, the SQL DAO will prefix this URL with the JDBC protocol to form the full URL jdbc:mysql://localhost:3306.

The URL you specify should also include in it’s path the database to be used by Para. The SQL DAO will not automatically create a database for you (though Para does create tables within your database automatically), so you must use an existing database. For example, you cannot simply specify the URL to your MySQL cluster/server (mysql://localhost:3306), but rather you need to specify the path to an existing database (mysql://localhost:3306/para). Note that the user name and password you provide with para.sql.user and para.sql.password should correspond to the specific database you specify in the URL, and that user should have complete permissions within that database.

Using a JDBC Driver

The SQL DAO uses JDBC to connect to your SQL database, which means a SQL driver (java.sql.Driver) will be needed for your chosen flavor of SQL (for example, com.mysql.jdbc.Driver is used for MySQL). You must specify the fully-qualified class name for your SQL driver. Upon initialization, the SQL DAO will attempt to load this driver and verify that it exists in the classpath. If the driver cannot be found, the SQL DAO will fail to initiailize and the DAO cannot be used.

In addition to specifying the driver name, you need to ensure the JAR file containing the SQL driver corresponding to your database is on your classpath when launching Para Server. The easiest way to do this is to add your SQL driver’s JAR file to the lib/ directory relative to the location of the Para Server WAR file para-x.y.z.war.

Working with Oracle database

To use Oracle DB you need to create a user (schema) for Para, with CREATE SESSION and CREATE TABLE privileges. You also need to enable writes on the USERS tablespace if you get an error like ora-01950: no privileges on tablespace 'users'.

CREATE USER para IDENTIFIED BY <password>;
GRANT CREATE SESSION, CREATE TABLE TO para;
ALTER USER para quota unlimited on USERS;

Then the configuration will look something like this:

para.sql.driver = "oracle.jdbc.OracleDriver"
para.sql.url = "oracle:thin:@127.0.0.1:1521/XE"
para.sql.user = "para"
para.sql.password = "secret"

If you are have a sysdba/sysoper type of user, you can set para.sql.user = "para as sysdba". The plugin has been tested with the Express edition of Oracle 18c database.

Schema

BREAKING CHANGE: The schema has changed in v1.30.0 - columns timestamp and updated were removed, column json_updates was added. H2DAO attempts to apply these changes automatically or error, but SqlDAO does not.

Execute the following statements one after another before switching to the new version:

ALTER TABLE {app_identifier} DROP COLUMN timestamp, updated;
ALTER TABLE {app_identifier} ADD json_updates NVARCHAR;

This is not required for tables created after v1.30.0.

Here’s the schema for each table created by Para:

CREATE TABLE {app_identifier} (
    id                        NVARCHAR NOT NULL,
    type                    NVARCHAR,
    name                    NVARCHAR,
    parentid            NVARCHAR,
    creatorid            NVARCHAR,
    json                    NVARCHAR,
    json_updates    NVARCHAR
)

H2 DB

This is the default database for object storage since v1.25. It is lightweight and embedded, which speeds up the startup of the server. All tables are stored inside a ./data folder by default. Keep in mind that each app has its own table which is created automatically when the app is created.

The implementing class H2DAO is part of the para-dao-sql plugin.

There are several configuration properties for H2 (these go in your application.conf file):

property description

para.db.dir

The data directory for storing DB files. Defaults to ./data.

para.db.hostname

The hostname of the server. Setting this will enable H2’s “server mode” and starts a TCP server. Defaults to blank.

para.db.user

The username with access to the database. Defaults to para.

para.db.password

The password. Defaults to secret.

para.db.tcpServer

Parameters for the H2 TCP server. Defaults to blank.

The H2 web console

Browsing the data stored in H2 is relatively simple. Any JDBC-compatible client will work. You can also download the H2 JAR package and execute the command java -jar h2*.jar. This will open up the web console where you can open the Para DB file. The default credentials are:

  • JDBC URL: jdbc:h2:file:/para_folder/data/para
  • User name: para
  • Password: secret

The URL for your DB file will be different and it should not end with .mv.db.

Backup, restore and migration

To make a backup copy of the entire Para dabatase, including all child app tables, run this command:

java -cp h2*.jar org.h2.tools.Script -url jdbc:h2:file:/para_folder/data/para -user para -password secret -script para.zip -options compression zip

To restore an existing backup, run:

java -cp h2*.jar org.h2.tools.RunScript -url jdbc:h2:file:/para_folder/data/para -user para -password secret -script para.zip -options compression zip

When upgrading to a new version of the H2 library, you might get an exception like Unsupported type 17 [1.4.200/3] and java.lang.IllegalStateException: Unable to read the page at position 2080825755576522. This means that you’re trying to access an H2 database with a version incompatible with the H2 version used by the para-dao-sql plugin. You can either downgrade the plugin version or migrate the H2 database to the new version.

Migrating an old version of the H2 database to a new one requires you to first backup the data using the old H2 engine JAR, then restore the data using the new engine JAR like so:

  1. Stop Para
  2. Open a terminal in the para_folder/data directory
  3. Download the H2 JARs and backup the para database:
    $ java -cp h2-{OLD_H2_VERSION}.jar org.h2.tools.Script -url jdbc:h2:./para -user para -password secret -script para.sql
  4. Delete the old files and create a new para database using the Shell tool:
    $ rm para.mv.db para.trace.db
    $ java -cp h2-{NEW_H2_VERSION}.jar org.h2.tools.Shell
  5. Restore the backup to the newly created para database
    $ java -cp h2-{NEW_H2_VERSION}.jar org.h2.tools.RunScript -url jdbc:h2:./para -user para -password secret -script para.sql
  6. Start Para again

This implementation is production-ready and ideal for local development and testing.

Search interface

Para indexes objects based on their type. For example, a User objects has a type field with the value “user”. Most of the search methods below ask for a type to search for (i.e. the type field acts as a filter).

The Search interface defines the following methods used for search:

P findById(String id)

Returns the object with the given id from the index or null.

List<P> findByIds(List<String> ids)

Returns all objects for the given ids.

List<P> findNearby(String type, String query, int radius, double lat, double lng)

Location-based search query.

List<P> findPrefix(String type, String field, String prefix)

Searches for objects containing a field (property) that starts with the given prefix.

List<P> findNestedQuery(String type, String field, String query)

Searches inside a nested field nstd (a list of objects).

List<P> findQuery(String type, String query)

The main search method. Follows the Lucene query parser syntax.

List<P> findSimilar(String type, String filterKey, String[] fields, String liketext)

“More like this” search.

List<P> findTagged(String type, String[] tags)

Search for objects tagged with a set of tags.

List<P> findTags(String keyword)

Shortcut method for listing all tags.

List<P> findTermInList(String type, String field, List<?> terms)

Searches for objects containing any of the terms in the given list (matched exactly).

List<P> findTerms(String type, Map<String, ?> terms, boolean matchAll)

Searches for objects containing all of the specified terms (matched exactly)

List<P> findWildcard(String type, String field, String wildcard)

A wildcard query like “example*”.

All methods accept an optional Pager parameter for paginating and sorting the search results.

Also there are a few methods for indexing and unindexing objects but you should avoid calling them directly. These methods are:

  • void index(ParaObject obj)
  • void indexAll(List<P> objects)
  • void unindex(ParaObject obj)
  • void unindexAll(List<P> objects)

You should not index/unindex your objects manually - Para does this for you.

Elasticsearch

Now part of the para-search-elasticsearch plugin.

Elasticsearch is the right choice as the search engine for Para in production. It supports Elasticsearch v7+ and uses the high level REST client (default).

Support for the transport client has been removed as it has been deprecated in Elasticsearch 7.0.

The Search interface is implemented in the ElasticSearch class.

There are several configuration properties for Elasticsearch (these go in your application.conf file):

property description

para.cluster_name

Elasticsearch cluster name. Default is para-prod when running in production.

para.es.use_nested_custom_fields

Switches between “normal” and “nested” indexing modes. Defaults to false.

para.es.async_enabled

Asynchronous operation when indexing/unindexing. Defaults to false.

para.es.shards

The number of shards per index. Used when creating an new index. Default is 5.

para.es.replicas

The number of copies of an index. Default is 0.

para.es.auto_expand_replicas

Automatically make a replica copy of the index to the number of nodes specified. Default is 0-1.

para.es.restclient_scheme

Scheme (for REST client). Default is https in production, http otherwise.

para.es.restclient_host

ES server host (for REST client). Default is localhost.

para.es.restclient_port

ES server port (for REST client). Default is 9200.

para.es.track_total_hits

If true, total hits are always counted accurately (slower queries), otherwise they are counted accurately up to 10000 documents (default, faster queries). If set to an integer, the results are counted accurately up to that integer. Default is blank.

para.es.fail_on_indexing_errors

If enabled, throws an exception if an error occurs during indexing operations. This will cascade back to clients as HTTP 500. Default is false.

para.es.bulk.size_limit_mb

BulkProcessor flush threshold in terms of megabytes. Default is 5.

para.es.bulk.action_limit

BulkProcessor flush threshold in terms of batch size. Default is 1000.

para.es.bulk.concurrent_requests

BulkProcessor concurrent requests (0 means synchronous execution). Default is 1.

para.es.bulk.flush_interval_ms

BulkProcessor flush interval in milliseconds. Default is 5000.

para.es.bulk.backoff_initial_delay_ms

BulkProcessor inital backoff delay in milliseconds. Default is 50.

para.es.bulk.max_num_retries

BulkProcessor number of retries. Default is 8.

para.es.bulk.flush_immediately

If set to true, BulkProcessor will flush immediately on each request, concurrently (in another thread). Default is true.

para.es.sign_requests_to_aws

If enabled, requests will be signed using the AWS V4 algorithm. Default is false.

para.es.aws_region

Used only for the purposes of signing requests to AWS. Default is null.

para.es.proxy_enabled

Enables the Elasticsearch proxy endpoint. Default is false.

para.es.proxy_path

The path to the proxy endpoint. Default is _elasticsearch.

para.es.root_index_sharing_enabled

Enable/disable root index sharing by child apps with isSharingIndex set to true. Default is false.

The plugin is on Maven Central. Here’s the Maven snippet to include in your pom.xml:

<dependency>
  <groupId>com.erudika</groupId>
  <artifactId>para-search-elasticsearch</artifactId>
  <version>{version}</version>
</dependency>

Note: There’s a fork of the plugin which is compatible only with the Elasticsearch 5.x branch and it’s missing some of the latest features like AWS Elasticsearch support and it uses only the legacy transport client. The project’s repository is at Erudika/para-search-elasticsearch-v5. The Maven coordinates for the legacy plugin are:

<dependency>
  <groupId>com.erudika</groupId>
  <artifactId>para-search-elasticsearch-v5</artifactId>
  <version>{version}</version>
</dependency>

Alternatively you can download the JAR from the “Releases” tab above put it in a lib folder alongside the server JAR file para-x.y.z.jar. Para will look for plugins inside lib and pick up the Elasticsearch plugin.

Finally, set the config property:

para.search = "ElasticSearch"

This could be a Java system property or part of a application.conf file on the classpath. This tells Para to use the Elasticsearch implementation instead of the default (Lucene).

Synchronous versus Asynchronous Indexing

The Elasticsearch plugin supports both synchronous (default) and asynchronous indexing modes. For synchronous indexing, the Elasticsearch plugin will make a single, blocking request through the client and wait for a response. This means each document operation (index, reindex, or delete) invokes a new client request. For certain applications, this can induce heavy load on the Elasticsearch cluster. The advantage of synchronous indexing, however, is the result of the request can be communicated back to the client application. If the setting para.es.fail_on_indexing_errors is set to true, synchronous requests that result in an error will propagate back to the client application with an HTTP error code.

The asynchronous indexing mode uses the Elasticsearch BulkProcessor for batching all requests to the Elasticsearch cluster. If the asynchronous mode is enabled, all document requests will be fed into the BulkProcessor, which will flush the requests to the cluster on occasion. There are several configurable parameters to control the flush frequency based on document count, total document size (MB), and total duration (ms). Since Elasticsearch is designed as a near real-time search engine, the asynchronous mode is highly recommended. Making occasional, larger batches of document requests will help reduce the load on the Elasticsearch cluster.

The asynchronous indexing mode also offers an appealing feature to automatically retry failed indexing requests. If your Elasticsearch cluster is under heavy load, it’s possible a request to index new documents may be rejected. With synchronous indexing, the burden falls on the client application to try the indexing request again. The Elasticsearch BulkProcessor, however, offers a useful feature to automatically retry indexing requests with exponential backoff between retries. If the index request fails with a EsRejectedExecutionException, the request will be retried up to para.es.bulk.max_num_retries times. Even if your use case demands a high degree of confidence with respect to data consistency between your database and index, it’s still recommended to use asynchronous indexing with retries enabled. If you’d prefer to use asynchronous indexing but have the BulkProcessor flushed upon every invocation of index/unindex/indexAll/unindexAll, simply enabled para.es.bulk.flush_immediately. When this option is enabled, the BulkProcessor’s flush method will be called immediately after adding the documents in the request. This option is also useful for writing unit tests where you want ensure the documents flush promptly.

Indexing modes

This plugin has two indexing modes: normal and nested. The nested mode was added after v1.28 to protect against a possible mapping explosion which happens when there are lots of objects with lots of different custom properties in them. This overloads the Elasticsearch index metadata and can crash the whole cluster. This indexing mode affects only custom properties in Sysprop objects.

The old “normal” mode is suitable for most Para deployments, with just a few tenants or a single tenant (one app per server). In this mode, Para objects are indexed without modification (all data types are preserved) but this could lead to a mapping explosion.

The nested data structure for these two indexing modes is shown below:

// NORMAL MODE                   // NESTED MODE
{                                {
  "id": "123",                     "id": "123",
  "appid": "para",                 "appid": "para",
  "type": "custom",                "type": "custom",
  "properties": {                  "properties": [
    "key1": "value1",                {"k": "key1",         "v": "value1"},
    "key2": {                        {"k": "key2-subkey1", "v": "subValue1"},
      "subkey1": "subValue1"         {"k": "numericKey3",  "vn": 5}
    },                             ],
    "numericKey3": 5               "_properties": "{\"key1\":\"value1\"}..."
  }                              }
}

Switching to the new nested indexing mode is done with the configuration property:

para.es.es.use_nested_custom_fields = true

Another benefit, when using the “nested” mode, is the support for nested queries in query strings. This is a really useful feature which, at the time of writing this, has not yet been implemented in Elasticsearch (issue elastic/elasticsearch#11322). Even better, you can query objects within nested arrays with pinpoint precision, e.g. ?q=properties.nestedArray[2].key:value. A nested query string query is detected if it contains a field with prefix properties.*. Examples of query string queries:

/v1/search?q=term AND properties.owner.age:[* TO 34]
/v1/search?q=properties.owner.name:alice OR properties.owner.pets[1].name=whiskers

Note: Sorting on nested fields works only with numeric data. For example, sorting on a field properties.year will work, but sorting on properties.month won’t (applicable only to the “nested” mode).

Calling Elasticsearch through the proxy endpoint

You can directly call the Elasticsearch API through /v1/_elasticsearch. To enable it, set para.es.proxy_enabled = true. Then you must specify the path parameter corresponds to the Elasticsearch API resource path. This is done for every GET, PUT, POST, PATCH or DELETE request to Elasticsearch. The endpoint accepts request to either /v1/_elasticsearch or /v1/_elasticsearch/{path} where path is a URL-encoded path parameter. Do not add query parameters to the request path with ?, instead, pass them as a parameter map.

GET /v1/_elasticsearch/_search
GET /v1/_elasticsearch/mytype%2f_search
DELETE /v1/_elasticsearch/tweet%2f1

ParaClient example:

Response get = paraClient.invokeGet("_elasticsearch/" + Utils.urlEncode("tweet/_search"), params);

Response post = paraClient.invokePost("_elasticsearch/_count",
                Entity.json(Collections.singletonMap("query",
                                        Collections.singletonMap("term",
                                        Collections.singletonMap("type", "cat")))));

If the path parameter is omitted, it defaults to _search.

The response object will be transformed to be compatible with Para clients an looks like this:

{
    "page":0,
    "totalHits":3,
    "items":[{...}]
}

If you wish to get the raw query response from Elasticsearch, add the parameter getRawResponse=true to the requst path and also URL-encode it:

GET /v1/_elasticsearch/mytype%2f_search%3FgetRawResponse%3Dtrue

Equivalently, the same can be done by adding the query parameter using ParaClient:

MultivaluedHashMap<String, String> params = new MultivaluedHashMap<>();
params.putSingle("getRawRequest", "true");
paraClient.invokeGet("_elasticsearch/" + Utils.urlEncode("mytype/_search"), params);

Note: This endpoint requires authentication and unsigned requests are not allowed. Keep in mind that all requests to Elasticsearch are prefixed with the app identifier. For example if the app id is “app:myapp, then Para will proxy requests to Elasticsearch at http://eshost:9200/myapp/{path}.

Rebuilding indices through the Elasticsearch proxy endpoint

You can rebuild the whole app index from scratch by calling POST /v1/_elasticsearch/reindex. To enable it set para.es.proxy_reindexing_enabled = true first. This operation executes ElasticSearchUtils.rebuildIndex() internally, and returns a response indicating the number of reindexed objects and the elapsed time:

{
   "reindexed": 154,
   "tookMillis": 365
}

Additionally, you can specify the destination index to reindex into, which must have been created beforehand:

POST /v1/_elasticsearch/reindex?destinationIndex=yourCustomIndex

Search query pagination and sorting

The Elasticsearch plugin supports two modes of pagination for query results. The standard mode works with the page parameter:

GET /v1/users?limit=30&page=2

The other mode is “search after” and uses a stateless cursor to scroll through the results. To activate “search after”, append the lastKey query parameter to the search request like so:

GET /v1/users?limit=10000&sort=_docid&lastKey=835146458100404225

Important: For consistent results when doing “search after” scrolling, set pager.setSortby("_docid") to sort on the _docid field. Additionally, there’s a limit to the result window imposed by Elasticsearch of maximum 10000 documents. See the docs for index.max_result_window.

The “search after” method works well for deep pagination or infinite scrolling or search results. The lastKey field is returned in the body of the response for each search query. It represents the _docid value for a Elasticsearch document - a unique, time-based long. You may have to rebuild your index for “search after” to work.

Sorting is done on the timestamp field by default, in desc (descending) order. To sort on a different field, set pager.setSortBy(field). Sorting on multiple fields is also possible by separating them with a comma. For example:

GET /v1/users?sort=name,timestamp
// or
pager.setSortBy("name:desc,timestamp:asc");

Shared indices with alias routing

The plugin also supports index sharing, whereby the root app index is shared with other apps which are created with the flag app.isSharingIndex = true. This feature is enabled with para.es.root_index_sharing_enabled = true and it is off by default. When the root index is created with sharing enabled, a special alias is created for it that contains a routing field which sends all documents of a child app to a particular shard, while providing total isolation between apps. This is useful when there are lots of smaller apps with just a few hundred documents each and we want to avoid the overhead of one index per app.

Read the Elasticsearch docs for more information.

Lucene

Lucene is used as the default search engine in Para. It is a lightweight alternative to Elasticsearch for self-contained deployments. It works great for local development and also in production.

The Search interface is implemented in the LuceneSearch class and is part of the para-search-lucene plugin.

Keep in mind that each Para app has its own Lucene index, which is automatically created if missing. The path to where Lucene files are stored is controlled by para.lucene.dir which defaults to . the current directory. If you set it to para.lucene.dir = "/home/user/lucene" index files will be stored in /home/users/lucene/data.

Search query pagination

Lucene supports two modes of pagination for query results. The standard mode works with the page parameter:

GET /v1/users?limit=30&page=2

The other mode is “search after” and uses a stateless cursor to scroll through the results. To activate “search after”, append the lastKey query parameter to the search request like so:

GET /v1/users?limit=10000&sort=_docid&lastKey=835146458100404225

Important: For consistent results when doing “search after” scrolling, set pager.setSortby("_docid") to sort on the _docid field.

The “search after” method works well for deep pagination or infinite scrolling or search results. The lastKey field is returned in the body of the response for each search query. It represents the _docid value for a Lucene document - a unique, time-based long. You may have to rebuild your index for “search after” to work.

Read the Lucene docs for more information.

Cache interface

The Cache interface defines the following methods used for object caching:

method description

void put(String id, T object)

Caches an object with a key equal to its id. Should skip null objects.

void putAll(Map<String, T> objects)

Caches multiple objects. Should skip null objects.

T get(String id)

Retrieves an object from cache. Returns null if the object isn’t cached.

Map<String, T> getAll(List<String> ids)

Retrieves multiple objects from cache.

void remove(String id)

Removes an object from cache.

void removeAll()

Clears the cache completely.

void removeAll(List<String> ids)

Clears only the objects with the specified ids.

You should avoid calling cache related methods directly - Para does this for you.

Caffeine

Caffeine is the default cache implementation since v1.26. It’s built on top of the excellent Caffeine library by Ben Manes. This cache supports automatic eviction and TTL for each object.

There’s one big cache map, shared by all Para apps. Cached objects have keys with a unique prefix for each app. When the cache reaches its maximum size or when objects in the map expire, the least recently used ones are evicted. Caffeine is simple and effective, making it excellent for smaller Para clusters or single-node deployments.

These are the configuration properties for Caffeine:

property description

para.caffeine.cache_size

Maximum size for the cache map. Defaults to 10000.

para.caffeine.evict_after_minutes

Cache eviction policy - objects are evicted after this time. Defaults to 10 min.

For more information see the Caffeine wiki.

Hazelcast

Hazelcast allows you to use a portion of the memory on each node for caching without having to manage a separate caching server or cluster. Hazelcast was the default implementation of the Cache interface until v1.26, when it was decoupled from Para and moved to its own repository. We still recommend the Hazelcast plugin for production use.

In Hazelcast, caches are organized by application id - each application has its own separate distributed map with the same name.

These are the configuration properties for Hazelcast:

property default value

para.hc.async_enabled

false

Asynchronous operation when putting objects in cache.

para.hc.eviction_policy

LRU

Cache eviction policy - LRU or LFU.

para.hc.eviction_percentage

25

Cache eviction percentage.

para.hc.ttl_seconds

3600

‘Time To Live’ for cached objects in seconds.

para.hc.max_size

25

Cache size as a percentage of used heap.

para.hc.jmx_enabled

true

Enables JMX reporting.

para.hc.ec2_discovery_enabled

true

Enables AWS EC2 auto discovery.

para.hc.discovery_group

hazelcast

Security group for cloud discovery of nodes.

para.hc.mancenter_enabled

!IN_PRODUCTION

Enables the Hazelcast Management Center.

para.hc.mancenter_url

http://localhost:8001/mancenter

The URL for the Management Center server.

In case you have enabled EC2 auto discovery, you must set para.hc.aws_access_key, para.hc.aws_secret_key and para.hc.aws_region accordingly.

For more information see the Hazelcast docs.

One-to-many

Object relationships are defined by the Linkable interface. All Para objects are linkable, meaning that they can be related to other Para objects.

Para supports one-to-many relationships between objects with the parentid field. It contains the id of the parent object. For example a user might be linked to their father like this:

+--------+
| Darth  |
| id: 5  |  Parent
+---+----+
    |
+---+---------+
| Luke        |
| id: 10      |  Child
| parentid: 5 |
+-------------+

This allows us to have a parent objects with many children which have the same parentid set. Now we can get all children for a given object by calling parent.getChildren(Class<P> clazz). This will return the list of objects that have a parentid equal to that object’s id. For example:

// assuming we have the parent object...
User luke = parent.getChildren(User.class).get(0);
User darth = dao.read(luke.getParentid());

Many-to-many

Many-to-many relationships are implemented in Para with Linker objects. This object contains information about a link between two objects. This is simply the id and type of both objects. Linker objects are just regular Para objects - they can be persisted, indexed and cached.

+--------+
|  tag1  |
+---+----+
    |
+---+------------+
|post:10:tag:tag1|  Linker
+---+------------+
    |
+---+------+
|  Post1   |
|  id:10   |
+----------+

Note: The following methods are only used when creating “many-to-many” links. Linking and unlinking two objects, object1 and object2, is done like this:

object1.link(object2.getType(), object2.getId());
object1.unlink(object2.getType(), object2.getId());
// delete all links to/from object1
object1.unlinkAll();

To check if two objects are linked use:

object1.isLinked(object2.getType(), object2.getId())

Also you can count the number of links by calling:

object1.countLinks(object2.getType())

Finally, to read all objects that are linked to object1, use:

object1.getLinkedObjects(object2.getType(), Pager... pager);

Collecting metrics

With version 1.27, a number of metrics are implemented on top of DropWizard. By default, DropWizard metrics are captured for every application, as well as the overall system across all applications. The DropWizard Timer is used to record metrics, which gives count and latency measurements. There are timers implemented on all of the following components:

  • All methods on the DAO interface
  • All methods on the Search interface
  • All methods on the Cache interface
  • All REST endpoints for CRUD operations (both single and batch)
  • All REST endpoints for Search operations
  • All REST endpoints for Link operations
  • All REST endpoints for classes implementing the CustomResourceHandler interface

All of the above metrics are initialized at startup for the overall system, as well as any existing applications. A new log file output was added to print the names of the applications found during initialization, and to indicate which one is the root app. If a new app is created, metrics are automatically initialized for this app as well.

Metrics are retrievable in two ways. First, a new log file called para-metrics.log is created by default. The system metrics are written to this file at a rate of every 60 seconds by default. There is a user configurable parameter para.metrics.logging_rate which will override the logging rate of the log file. If the logging rate is set to zero, no metrics are saved to the log file.

The other alternative for retrieving metrics is by configuring Para to push them to a metrics server like Graphite. This method is preferred over pulling metrics from the API, because in a distributed environment with multiple Para nodes and load balancers it becomes cumbersome to aggregate the metrics from all nodes.

There are several configuration settings for metrics in Para:

# enable/disable the collection of metrics (default: true)
para.metrics_enabled = false
# enable/disable JMX reporting (default: false)
para.metrics.jmx_enabled = false
# The URL of the host to push metrics to (default: blank)
para.metrics.graphite.host = "localhost"
# The port number of the Graphite server
para.metrics.graphite.port = 2003
# The prefixes for applying to metric names (default: blank)
para.metrics.graphite.prefix_system = "com.erudika.para.{{INSTANCE_ID}}"
para.metrics.graphite.prefix_apps = "com.erudika.para.{{INSTANCE_ID}}.{{APP_ID}}"
# The period for how often to push system metrics in seconds (default: 0, disabled)
para.metrics.graphite.period = 0

The variable para.metrics.graphite.period controls how often (in seconds) metrics data is pushed to the Graphite server. This field controls the frequency of metrics reporting for both the system-wide metrics as well as any app-specific metrics that are pushed to Graphite. The default value is 0, which disables all push to Graphite. Settings this value to a positive number (i.e. 60) will allow for system metrics to be pushed to a Graphite server, as well as application metrics.

System-wide metrics are only reported to a Graphite server if para.metrics.graphite.host is not blank (it defaults to null). This field specifies the host of the Graphite server to push only system metrics to and para.metrics.graphite.port is the corresponding port number to the host (usually 2003 for Graphite).

Graphite metrics are often prefixed by some path to distinguish application metrics from other applications pushing to the same Graphite server. For example, you may want to prefix your Para application metrics with “com.erudika.para”. There are two fields for configuring a prefix for pushing metrics to Graphite. First, para.metrics.graphite.prefix_apps indicates the prefix that should be applied when pushing the system-wide metrics. Second, para.metrics.graphite.prefix_apps indicates the template prefix that should be applied when pushing a specific application’s metrics to Graphite.

It’s common practice in a distributed environment to include the instance ID in the prefix for pushing metrics data. That makes it possible to view metrics for each node in your cluster, and perform an aggregation across all nodes. This is supported by defining the instance ID for your system in an environment variable called para.instance_id. If you define para.instance_id, you can reference it in the para.metrics.graphite.prefix_system and para.metrics.graphite.prefix_apps variables. To include the instance ID, simply add {{INSTANCE_ID}} in your prefix variables and Para will replace it with the contents of para.instance_id. For example, a system prefix variable may be configured as follows in your application config file:

para.instance_id = "1234abcd"
para.metrics.graphite.prefix_system = "com.erudika.para.{{INSTANCE_ID}}.system"

In para.metrics.graphite.prefix_apps you can use both the {{INSTANCE_ID}} placeholder as well as the application’s identifier placeholder - {{APP_ID}}. This allows application-specific metrics to contain the application’s ID in the prefix path. For example, an application prefix variable may be configured as follows in your application config file:

para.metrics.graphite.prefix_apps = "com.erudika.para.{{INSTANCE_ID}}.{{APP_ID}}"

Configuring the Graphite host settings for a specific application can be done by adding a setting to the application (see app settings API). Para will look for an application setting by the name “metricsGraphiteReporter” to detect application-specific Graphite settings. This setting is configured in the form of a map with two fields: host (String) and port (int). For example, to add application-specific metrics make a signed request to Para as follows:

PUT /v1/_settings/metricsGraphiteReporter
{
  "value": {
    "host": "localhost",
    "port": 2003
  }
}

Running Graphite and Grafana locally

To run a local Graphite server start a docker container like hopsoft/docker-graphite-statsd and then run a local Grafana server using the instructions at http://docs.grafana.org/installation/docker/.

Here’s how to run Graphite with a Docker command:

$ docker run -d --name graphite --restart=always -p 80:80 -p 2003-2004:2003-2004 -p 2023-2024:2023-2024 \
  -p 8125:8125/udp -p 8126:8126 graphiteapp/graphite-statsd

Then point you browser to http://localhost/. Then run Grafana locally with:

$ docker run -d -p 3000:3000 -e GF_SECURITY_ADMIN_USER=admin -e GF_SECURITY_ADMIN_PASSWORD=password \
  -e GF_METRICS_GRAPHITE_ADDRESS=localhost:2003 grafana/grafana

Then open Grafana at http://localhost:3000 and login using admin/password at the login screen. Finally, add Graphite as a data source to Grafana by specifying the Graphite server location at http://localhost using “Browser” access.

Instrumenting Para plugins

You can easily measure any aspect of a Para plugin using the static methods Metrics.counter() and Metrics.time():

// call this from a method to increment a counter each time it's called
Metrics.counter(appid, getClass(), "searchQueryCalled").inc();

// use this to time execution of a block of code
try (Metrics.Context ctx = Metrics.time(appid, getClass(), "timedBlock")) {
    // do stuff...
}

Health check

A simple health check is mapped to /v1/_health which returns 200 OK if the server is “healthy”, and 500 if it’s “unhealthy”. The health check is configured to simply read the root app object from the DAO, Search and Cache to ensure there are active connections with each resource. The implementation is done in a class called HealthUtils in the “utils” package of para-server.

The health check endpoint is publicly accessible so it can be easily called by any application or a load balancer. To prevent excessive calls to the health check end point, the health check method is only allowed to run at a fixed interval (defaults to once per minute, but can be configured using para.health.check_interval).

An initial health check is done on startup and a warning is printed if Para is not initialized, which is actually just an assumption at this point. The only way to distinguish between “not initialized” and “unhealthy” is to check for connection errors in the logs. Para starts as unhealthy, but once the root app is created, i.e. /v1/_setup is called, the status is updated to “healthy”.

Here’s how to programmatically check the health of the system:

HealthUtils.getInstance().performHealthCheck();
boolean isHealthy = HealthUtils.getInstance().isHealty();

To disable health checking, set para.health_check_enabled = false.

Logging

Para uses SLF4J as a logging abstraction framework, meaning you can plug in any logging implementation like Logback, Log4j2, etc. By default Para uses Logback. To use another implementation, exclude all Logback packages from pom.xml like this:

<dependency>
  <groupId>com.erudika</groupId>
  <artifactId>para-core</artifactId>
  <version>${para.version}</version>
  <exclusions>
    <exclusion>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-core</artifactId>
    </exclusion>
    <exclusion>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
    </exclusion>
    <exclusion>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-access</artifactId>
    </exclusion>
  </exclusions>
</dependency>

Logs are stored in 3 files, located in the same directory from which Para is started:

  • para.log - main log file, also written to syslog
  • para-metrics.log - contains all the metrics
  • para-access.log - contains a history of requests

To change the directory where logs are stored, set the system property para.logs_dir to a folder of your choice (should not end with “/“):

$ java -jar -Dpara.logs_dir=/var/logs/para para.jar

Translations

Translations are Para objects which contain a single translation of a string. For example you can have a language file in English and for each string you can create a Translation object with the translation of that string.

Translations have a key and a value. The key is used to identify the string to be translated. Each translation also has a specific locale associated with it.

A translation can be approved which makes it possible to build a crowdsourced system for translation of language packs.

Languages

Languages are maps of keys and values. A language file might contain all strings used by an application. Also you can have the language loaded from a database.

The language map consists of language strings and each string has a short unique key. For example:

english.txt
-----------
yes = "Yes"
no  = "No"

italian.txt
-----------
yes = "Sì"
no  = "No"

The LanguageUtils class deals with the loading of languages and contains methods for working with locales. You can set default languages, write languages to a database and load them.

Filters

There are several filters you can use to enable different features like Gzip encoding, CORS, etc.

CORS

The CORS filter enables cross-origin requests for the Para API. It is enabled with the configuration property para.cors_enabled set to true. The implementation is based on the open source CORSFilter by Ebay.

Gzip

The GZipServletFilter is enabled by para.gzip_enabled and provides on-the-fly Gzip encoding for static resources, as well as, the Para API JSON responses.

Caching

The CachingHttpHeadersFilter is not used by Para but you can use it to enable Cache-Control headers for all static resources.

Misc. utilities

The Utils static class contains a variety of utility methods. These are summarized below:

Hashing

String MD5(String s)

Calculates the MD5 hash for a string.

String bcrypt(String s)

Calclates the BCrypt hash for a string. Based on Spring Security.

boolean bcryptMatches(String plain, String storedHash)

Validates a BCrypt hash.

Strings

String stripHtml(String html)

Strips all HTML tags from a string leaving only the text.

String markdownToHtml(String markdownString)

Convert a Markdown string to HTML. Based on Txtmark. Supports GFM syntax, tables, task lists, striketrough and emojis.

String compileMustache(Map<String, Object> scope, String template)

Compile a Mustache template string to HTML. Based on Mustache.java.

String abbreviate(String str, int max)

Abbreviates a string to a given length.

String stripAndTrim(String str)

Removes punctuation and symbols and normalizes whitespace.

String noSpaces(String str, String replaceWith)

Spaces are replaced with something or removed completely.

String formatMessage(String msg, Object... params)

Replaces placeholders like {0}, {1}, etc. with the corresponding objects (numbers, strings, booleans).

Numbers

String formatPrice(double price)

Formats a price to a decimal with two fractional digits.

double roundHalfUp(double d)

Rounds up a double using the “half up” method, scale is 2 fractional digits.

double roundHalfUp(double d, int scale)

Rounds up a double using the “half up” method.

String abbreviateInt(Number number, int decPlaces)

Rounds a number like “10000” to “10K”, “1000000” to “1M”, etc.

Dates

String formatDate(Long timestamp, String format, Locale loc)

Formats a date according to a specific locale.

String[] getMonths(Locale locale)

A list of the twelve months in the language set by the locale.

JSON

P fromJSON(String json)

Converts a JSON string to a ParaObject. Requires the type property. Based on Jackson.

String toJSON(P obj)

Converts a ParaObject to a JSON string. Based on Jackson.

Objects

boolean typesMatch(ParaObject so)

Validates that an object’s class matches its type property.

Map<String, Object> getAnnotatedFields(P bean)

Returns a map of all fields marked with the Stored annotation for a given object.

P setAnnotatedFields(Map<String, Object> data)

Reconstruct an object from a map of fields and their values.

P toObject(String type)

Constructs a new instance of the given type (must be a known ParaObject).

Class<? extends ParaObject> toClass(String type)

Returns the Class instance of a given type (must be a known ParaObject).
boolean isBasicType(Class<?> clazz)
Checks if a class is primitive, String or a primitive wrapper.

String[] validateObject(ParaObject content)

Runs the Hibernate Validator against an object. Returns a list of errors or an empty array if that object is valid.

String getNewId()

Generates a new id. Based on Twitter’s Snowflake algorithm. You must set para.worker_id to be different on each node.

String type(Class<? extends ParaObject> clazz)

URLs

boolean isValidURL(String url)

Validates a URL.

String getHostFromURL(String url)

Returns the host name of a URL - “example.com”

String getBaseURL(String url)

Returns the base URL, e.g. http://example.com

HumanTime

Para includes the class HumanTime written by Johann Burkard. This class makes it easy to convert a timestamp to an approximation in the form of “X minutes ago”.

For example you can do:

HumanTime ht = Utils.getHumanTime();
// prints 15 h 45 m
String s1 = ht.approximately(56720083L);

int timeOfEvent = 1399554448;
int timeNow = System.currentTimeMillis();
// calculate elapsed time
String s2 = ht.approximately(timeNow - timeOfEvent);
System.out.println("Event happened " + s2 + " time ago");

For more information about HumanTime see the docs.

Pager

The Pager class is used for pagination. It holds data about a page request. For example you can call a search method like this:

Pager pager = new Pager();
// limit results to 5
pager.setLimit(5);
// sort by the 'tag' field
pager.setSortby("tag");
// descending order
pager.setDesc(true);
// last key special field
pager.setLastKey("1234");
List<Tag> tags = search.findTags("tag1", pager);
// the total number of tags for the query
int tagCount = pager.getCount();

The Pager class has a special string field lastKey for storing the last id for scrolling pagination queries. Usually scrolling is used in NoSQL databases, where in order to get a page of results, you have to provide the last key from the previous page.

Note: The fields page and limit are constrained by para.max_pages = 1000 and para.max_page_limit = 256. The first controls the maximum page number, and the second one sets the maximum results per page (i.e. the maximum value which the limit parameter can have).

Pager objects are used primarily in combination with search queries and allow you to limit the results of a query. When using ParaClient in combination with Pager, the client will automatically set the pager.lastKey property. To use the standard pagination mode, you must explicitly set the pager.page property:

// fetch page 1
paraClient.findQuery("*", pager);
pager.setPage(2);

// fetch page 2
paraClient.findQuery("*", pager);
pager.setPage(3);
//  and so on...

For “search after”, or “deep” pagination the page property is omitted:

// fetch page 1
paraClient.findQuery("*", pager);

// fetch page 2
paraClient.findQuery("*", pager);
//  and so on...

And here’s an example for reading all object of type “cat” using deep pagination (“search after”):

Pager pager = new Pager(1, "_docid", false, 10_000);
List<Cat> cats = new LinkedList<>();
List<Cat> catsPage;
do {
    catsPage = Para.getSearch().findQuery("cat", "*", pager);
    cats.addAll(catsPage);
} while (!catsPage.isEmpty());

Email

The Emailer interface has a simple API for sending email messages.

public interface Emailer {
    boolean sendEmail(List<String> emails, String subject, String body);
}

Para can either use the JavaMail API or AWS SES to send emails. This is used for email verification, password recovery and notifications. Set support_email to be the email address used by the system. An example config for JavaMail:

para.emailer = "javamail"
para.support_email = "[email protected]"
para.mail.host = "smtp.example.com"
para.mail.port = 587
para.mail.username = "[email protected]"
para.mail.password = "password"
para.mail.tls = true
para.mail.ssl = false

Email templates can be loaded with:

Emailer.class.getClassLoader().getResourceAsStream("emails/template.html")

Para supports basic variable substitutions through Mustache with Utils.compileMustache(data, template).

Set para.emailer = "aws" to use the AWS Simple Email Service and comment out the para.mail.* properties as they are ignored. AWS credentials are picked up from the environment, like the AWS_PROFILE env. variable.

Queue

The Queue interface is another simple API for pushing and pulling messages to and from a queue.

public interface Queue {
    String pull();
    void push(String task);
    String getName();
    void setName(String name);
    void startPolling();
    void stopPolling();
}

Currently this interface is implemented by the AWSQueue class which relies on the AWS Simple Queue Service, and by LocalQueue which is based on a ConcurrentLinkedQueue. The implementation to load is configurable through:

para.q = "sqs"
# or
para.q = "local"

AWSQueue

The AWS SQS plugin uses Amazon’s SQS as a river by long polling for messages from a given queue. This particular implementation was adapted from an earlier project called elasticsearch-river-amazonsqs.

The code continuously pulls messages from an SQS queue. After a message is processed, it gets deleted from the queue. To enable this functionality, set para.queue_link_enabled = true in the Para configuration file.

Sample code:

AWSQueue q = new AWSQueue("myQueue");
AWSQueueUtils.pushMessages(q.getUrl(), listOfMessages);
List<String> result = AWSQueueUtils.pullMessages(q.getUrl(), 10);

To configure, put this in your application.conf:

para.queue.polling_sleep_seconds: 60
para.queue.polling_interval_seconds: 20

Messages are in the same format as all other Para objects (see Sysprop):

{
    "id": "id_OR_null",
    "appid": "para_app_id",
    "type": "para_data_type",
    "properties": { "key1": "value1" ...}
}

Notes:

  • If a '_delete': true field exists, the object in the message will be deleted.
  • If '_create': true field exists, the object will be recreated, overwriting anything with the same id.

When the queue is empty the river will sleep for sleep seconds before sending a new request for messages to the queue. Long polling is done by the Amazon SQS client using the waitTimeSeconds attribute which is set to longpolling_interval (must be between 0 and 20).

LocalQueue

This queue implementation is good for local development, testing and single-machine Para servers. It uses an in-memory queue which is not persisted in any way.

Storage

The FileStore interface allows you to have different storage services, like S3 or Google Drive, connected to your application. You can save and load files from disk or cloud storage services easily.

public interface FileStore {
    InputStream load(String path);
    String store(String path, InputStream data);
    boolean delete(String path);
}

AWSFileStore

This implementation uses AWS S3 as file storage location. The location of each file is controlled by the bucket name cofiguration property para.s3.bucket. There is also the para.s3.max_filesize_mb property which restricts the size of the upload.

Before each upload, files are prepended with a timestamp, for example myfile.txt becomes 1405632454930.myfile.txt. Currently all files are set to be stored with “Reduced Redundancy” turned on.

You can Gzip compress files before uploading uploading but you must append the .gz extension in order for the correct content encoding header to be set. The extension is removed before upload.

LocalFileStore

This implementation stores files on the local file system. The folder where files will be stored is set with the para.localstorage.folder property. The maximum file size is controlled by para.localstorage.max_filesize_mb.

Custom authentication

Para supports custom authentication providers through its “passwordless” filter. This means that you can send any user info to Para and it will authenticate that user automatically without passwords.

This mechanism is implemented in the PasswordlessAuthFilter class.

The default URL for this filter is /passwordless_auth and all requests to this location will be intercepted and processed by it. It accepts the following parameters:

  • token - a JWT generated on an external backend (for example your login server).
  • appid - the Para app identifier
  • redirect - if set to false redirects are disabled and JWT is returned in response body

Also see the configuration properties para.security.signin_success and para.security.signin_failure in section Config. These can be set for each app individually as signin_success and signin_failure in the app’s settings. For apps other than the root app use /passwordless_auth?appid=myapp.

You can disable redirects and configure the filter to return the authentication JWT directly in the response body with the request parameter ?redirect=false. If the authentication is succesful, a text/plain response is returned containing the actual Para JWT which can later be presented to the /v1/_me endpoint for checking if a user is logged in.

Custom authentication flow:

  1. A user wants to sign in to Para and clicks a button
  2. The button redirects the user to a remote login page you or your company set up.
  3. The user enters their credentials and logs in.
  4. If the credentials are valid, you send back a special JSON Web Token (JWT) to Para with the user’s basic information.
  5. Para verifies the token and if it’s valid, the request is redirected to the signin_success URL, otherwise to the signin_failure URL

The JWT must contain the following claims:

  • email - user’s email address
  • name - user’s display name
  • identifier - some unique ID for that user in the format custom:1234
  • appid - the Para app identifier (optional)

The JWT signature is verified using the secret key value which you provide in your configuration:

  • for the root app app:para set para.app_secret_key = "long_random_string"
  • for child apps - add a property to the app’s settings field:
    {
      "id": "app:myapp",
      "settings": {
          "app_secret_key": "long_random_string"
      }
    }
    This key must be at least 32 symbols in length and random. This key is different from the Para secret key for your app. The JWT should have a short validity period (e.g. 10 min). The JWT should also contain the claims iat and exp and optionally nbf.

Once you generate the JWT on your backend (step 4 above), redirect the successful login request back to Para:

GET https://para-host/passwordless_auth?appid=myapp&token=eyJhbGciOiJIUzI1NiI..

Simple authentication

Para implements several authentication mechanisms which you can integrate in your application and make it easy to handle user registrations and logins.

The classic way of logging users in is with usernames and passwords. Para implements that mechanism in the PasswordAuthFilter class. It takes a username or email and a password and tries to find that user in the database. If the user exists then it validates that the hash of the given password matches the hash in the database. The hashing algorithm is BCrypt.

The default URL for this filter is /password_auth and all requests to this location will be intercepted and processed by it. The default parameters to pass to it are email and password.

Also see the configuration properties para.security.signin_success and para.security.signin_failure in section Config. These can be set for each app individually as signin_success and signin_failure in the app’s settings. For apps other than the root app use /password_auth?appid=myapp.

Here’s an example HTML form for initiating password-based authentication:

<form method="post" action="/password_auth">
    <input type="email" name="email">
    <input type="password" name="password">
    <input type="submit">
</form>

Creating users programmatically

You can create users from your Java code by using a ParaClient. By default, users are created with active = false, i.e. the account is locked until the email address is verified. You can disable email verification with para.security.allow_unverified_emails = true. Another way to get around this is to “verify” the user manually, like so:

// user is created but account is locked
paraClient.signIn("password", "[email protected]:Morgan Freeman:pass123");
// read identifier first to get the user id
ParaObject identifier = paraClient.read("[email protected]");
User user = paraClient.read(identifier.getCreatorid());
user.setActive(true);
User updated = paraClient.update(user); // user is now active

After executing the code above, any subsequent calls to paraClient.signIn() will be successful and the authenticated user object will be returned.

SAML support

Para can act as a SAML service provider and connect to a specified identity provider (IDP). The implementation uses OneLogin’s SAML Java Toolkit. Here’s a summary of all the steps required to authenticate users with SAML:

  1. Generate a X509 certificate and private key for your SP
  2. Convert the private key to PKCS#8 and Base64-encode both public and private keys
  3. Specify the metadata URL for your IDP in your config file
  4. Register Para with your IDP as trusted SP by copying the metadata from /saml_metadata/{appid}
  5. Send users to /saml_auth/{appid} and Para will take care of the rest

The SP metadata endpoint is /saml_metadata/{appid} where appid is the app id for your Para app. For example, if your Para endpoint is paraio.com and your appid is scoold, then the metadata is available at https://paraio.com/saml_metadata/scoold as an XML file.

SAML authentication is initiated by sending users to the Para SAML authentication endpoint /saml_auth/{appid}. For example, if your Para endpoint is paraio.com and your appid is scoold, then the user should be sent to https://paraio.com/saml_auth/scoold. Para (the service provider) will handle the request and redirect to the SAML IDP. Finally, upon successful authentication, the user is redirected back to https://paraio.com/saml_auth/scoold which is also the assertion consumer service (ACS).

Note: The X509 certificate and private key must be encoded as Base64 in the configuration file. Additionally, the private key must be in the PKCS#8 format (---BEGIN PRIVATE KEY---). To convert from PKCS#1 to PKCS#8, use this:

openssl pkcs8 -topk8 -inform pem -nocrypt -in sp.rsa_key -outform pem -out sp.pem

There are lots of configuration options but Para needs only a few of those in order to successfully authenticate with your SAML IDP (listed in the first rows below).

property default value

para.security.saml.sp.entityid

blank
The SP entityId, a URL in the form of https://paraio.com/saml_auth/{appid}.

para.security.saml.sp.assertion_consumer_service.url

blank
The URL where users will be redirected back to, from the IDP. Same value as the entityId above.

para.security.saml.sp.nameidformat

blank
Specifies constraints on the name identifier to be used to represent the requested subject.

para.security.saml.sp.x509cert

blank
The X509 certificate for the SP, encoded as Base64.

para.security.saml.sp.privatekey

blank
The private key for the X509 certificate, in PKCS#8 format, encoded as Base64.

para.security.saml.idp.entityid

blank
The IDP entityId, a URL in the form of https://idphost/idp/metadata.xml

para.security.saml.idp.single_sign_on_service.url

blank
SSO endpoint URL of the IdP.

para.security.saml.idp.x509cert

blank
The x509 certificate for the IDP, encoded as Base64.

para.security.saml.idp.metadata_url

blank
The location of IDP’s metadata document. Para will fetch it and the IDP will be auto-configured.

para.security.saml.security.authnrequest_signed

false
Enables/disables signing of auth requests to the IDP.

para.security.saml.security.want_messages_signed

false
Enables/disables signing of messages to the IDP.

para.security.saml.security.want_assertions_signed

false
Enables/disables the requirement for signed assertions.

para.security.saml.security.want_assertions_encrypted

false
Enables/disables the requirement for encrypted assertions.

para.security.saml.security.want_nameid_encrypted

false
Enables/disables the requirement for encrypted nameId

para.security.saml.security.sign_metadata

false
Enables/disables signing of SP’s metadata.

para.security.saml.security.want_xml_validation

true
Enables/disables XML validation by the SP.

para.security.saml.security.signature_algorithm

blank
Algorithm that the SP will use in the signing process.

para.security.saml.attributes.id

blank
Mapping key for the id attribute.

para.security.saml.attributes.picture

blank
Mapping key for the picture attribute.

para.security.saml.attributes.email

blank
Mapping key for the email attribute.

para.security.saml.attributes.name

blank
Mapping key for the name attribute. If this is set, the values for attributes firstname and lastname below will be ignored.

para.security.saml.attributes.firstname

blank
Mapping key for the firstname attribute.

para.security.saml.attributes.lastname

blank
Mapping key for the lastname attribute.

para.security.saml.domain

blank
Domain name for users who don’t have a valid email.

As a bare minimum, you should have the following SAML configuration:

# minimal setup
# IDP metadata URL, e.g. https://idphost/idp/shibboleth
para.security.saml.idp.metadata_url = ""

# SP endpoint, e.g. https://paraio.com/saml_auth/scoold
para.security.saml.sp.entityid = ""

# SP public key as Base64(x509 certificate)
para.security.saml.sp.x509cert = ""

# SP private key as Base64(PKCS#8 key)
para.security.saml.sp.privatekey = ""

# attribute mappings (usually required)
# e.g. urn:oid:0.9.2342.19200300.100.1.1
para.security.saml.attributes.id = ""
# e.g. urn:oid:0.9.2342.19200300.100.1.3
para.security.saml.attributes.email = ""
# e.g. urn:oid:2.5.4.3
para.security.saml.attributes.name = ""

You want to either configure para.security.saml.attributes.name or para.security.saml.attributes.firstname, but not both.

You can also configure the SAML authentication filter through the app settings API:

{
    "security.saml.sp.entityid": "https://paraio.com/saml_auth/scoold",
    "security.saml.idp.metadata_url": "https://idphost/idp/shibboleth",
    "security.saml.sp.x509cert": "LS0tLS1CRUdJTiBDRVJUSUZJQ0...",
    "security.saml.sp.privatekey": "LS0tLS1CRUdJTiBQUklWQVRF...",
    ...
    "signin_success": "http://success.url",
    "signin_failure": "http://failure.url"
}

Para can return a short-lived ID token back to the client which initiated the request. Add jwt=id to your signin_success url. For example { "signin_success": "http://success.url?jwt=id" }. The id value of the parameter will be replaced with the actual JWT ID token, which you can use to call paraClient.signIn("passwordless", idToken) to get the long-lived session token.

You can also put all of the settings above in a configuration file, but this only works if your app is the root app (see the config).

LDAP support

Users can be authenticated through an LDAP server, including Active Directory. The implementation uses the UnboundID SDK in combination with Spring Security. The user supplies a uid and password and Para connects to the LDAP server and tries to bind that user. Then, upon successful login, a new User object is created and the user is signed in. The user’s profile data (email, name, uid) is read from the LDAP directory. It is important to note that emails are not validated and are assumed valid.

Support for LDAP authentication is implemented by the LdapAuthFilter. The default URL for this filter is /ldap_auth. The filter takes two query parameters username and password and answers to any HTTP method. Example: GET /ldap_auth?username=bob&password=secret

These are the configuration options for this filter:

property default value

para.security.ldap.server_url

ldap://localhost:8389/

URL of the LDAP server, including scheme and port.

para.security.ldap.base_dn

dc=springframework,dc=org

The base DN, aka domain.

para.security.ldap.bind_dn

-
The initial bind DN for a user with search privileges. The value of this property cannot contain whitespaces. Those will automatically be escaped with %20. Usually this value is left blank.

para.security.ldap.bind_pass

-
The password for a user with search privileges. Usually this value is left blank.

para.security.ldap.user_search_base

-
Search base for user searches.

para.security.ldap.user_search_filter

(cn={0})

Search filter for user searches. This is combined with user_search_base to form the full DN path to a person in the directory. A user search is performed when the person cannot be found directly using user_dn_pattern + base_dn.

para.security.ldap.user_dn_pattern

uid={0}

DN pattern for finding users directly. This is combined with base_dn to form the full DN path to a person in the directory. This property can have multiple values separated by |, e.g. uid={0}|cn={0}.

para.security.ldap.password_attribute

userPassword

The password attribute in the directory

para.security.ldap.username_as_name

false

Use a person’s username as their name

para.security.ldap.active_directory_domain

-
The domain name for AD server. If blank (defaut) AD is disabled, unless ad_mode_enabled is true.

para.security.ldap.ad_mode_enabled

-
Explicitly enables support for authenticating with Active Directory. If true AD is enabled.

para.security.ldap.mods_group_node

-
Maps LDAP node like cn=Mods to Para user group mods.

para.security.ldap.admins_group_node

-
Maps LDAP node like cn=Admins to Para user group admins.

para.security.ldap.compare_passwords

-
If set to any value, will switch to password comparison strategy instead of default “bind” method.

Note: Active Directory support is enabled when active_directory_domain is set. For AD LDAP, the search filter defaults to (&(objectClass=user)(userPrincipalName={0})). The syntax for this allows either {0} (replaced with username@domain) or {1} (replaced with username only). For regular LDAP, only {0} is a valid placeholder and it gets replaced with the person’s username.

You can also configure LDAP through the app settings API:

{
    "security.ldap.server_url": "ldap://localhost:8389/",
    "security.ldap.base_dn": "dc=springframework,dc=org",
    "security.ldap.user_dn_pattern": "uid={0}"
    ...
    "signin_success": "http://success.url",
    "signin_failure": "http://failure.url"
}

You can also put all of the settings above in a configuration file, but this only works if your app is the root app (see the config).

OAuth 2.0 support

A generic OAuth 2.0 filter is available for situations where you need to use your own authentication server. It follows the standard OAuth 2.0 flow and requires that you redirect users to a login page first (on your server). Then, upon successful login, the user is redirected back to /oauth2_auth and an access token is obtained. Finally, the filter tries to get the user’s profile with that token, from a specified server, which could be the same server used for authentication.

Support for generic OAuth authentication is implemented by the GenericOAuth2Filter. The default URL for this filter is /oauth2_auth.

The endpoint expects an appid value from the ‘state’ parameter, e.g. ?state={appid}. If that parameter is missing, the default (root) app will be used as authentication target.

These are the configuration options for this filter:

property description

para.security.oauth.profile_url

API endpoint for user profile.

para.security.oauth.token_url

The URL from which the access token will be requested (via POST).

para.security.oauth.scope

The scope parameter of the access token request payload.

para.security.oauth.accept_header

The Accept header - if blank, the header won’t be set. (default is blank).

para.security.oauth.download_avatars

If true, Para will fetch profile pictures from IDP and store them locally or to a cloud storage provider. (default is false).

para.security.oauth.parameters.id

The id parameter for requesting the user id (default is sub).

para.security.oauth.parameters.picture

The picture parameter for requesting the user’s avatar (default is picture).

para.security.oauth.parameters.email

The email parameter for requesting the user’s email (default is email).

para.security.oauth.parameters.name

The name parameter for requesting the user’s full name (default is name).

para.security.oauth.parameters.given_name

The given_name parameter for requesting the user’s first name (default is given_name).

para.security.oauth.parameters.family_name

The family_name parameter for requesting the user’s last name (default is family_name).

para.security.oauth.domain

This domain name is used if a valid email can’t be obtained (optional).

para.security.oauth.token_delegation_enabled

Enable/disable access token delegation. If enabled, access tokens will be saved inside the user object’s password field and sent for validation to the IDP on each authentication request (using JWTs). (default is false).

You can configure the URLs for authentication success and failure in the configuration file (see the config)

You can also set all configuration properties through the app settings API:

{
    "oa2_app_id": "some_appid",
    "oa2_secret": "some_secret",
    "security.oauth.token_url": "https://myidp.com/oauth/token",
    "security.oauth.profile_url": "https://myidp.com/oauth/userinfo",
    "security.oauth.scope": "openid email profile",
    ...
    "signin_success": "http://success.url",
    "signin_failure": "http://failure.url"
}

You can add two additional custom OAuth 2.0/OpenID connect providers called “second” and “third”. Here’s what the settings look like for the “second” provider (“third” is identical but replace “second” with “third”):

{
    "oa2second_app_id": "some_appid",
    "oa2second_secret": "some_secret",
    "security.oauthsecond.token_url": "https://myidp.com/oauth/token",
    "security.oauthsecond.profile_url": "https://myidp.com/oauth/userinfo",
    "security.oauthsecond.scope": "openid email profile",
    ...
}

The endpoints for the “second” and “third” OAuth 2.0 providers are /oauth2second_auth and /oauth2third_auth, respectively.

Facebook support

This describes the web authentication flow with Facebook. You could also login with an existing access token from Facebook through the API. This web flow sets a cookie, the API returns a JWT instead.

First of all you need to have your API credentials ready by creating an app in the Facebook Dev Center. Then add them to your application.conf configuration file:

para.fb_app_id = "..."
para.fb_secret = "..."

Or add these through the app settings API:

{
    "fb_app_id": "..."
    "fb_secret": "..."
    "signin_success": "http://success.url"
    "signin_failure": "http://failure.url"
}

Para can return a short-lived ID token back to the client which initiated the request. Add jwt=id to your signin_success url. For example { "signin_success": "http://success.url?jwt=id" }. The id value of the parameter will be replaced with the actual JWT ID token, which you can use to call paraClient.signIn("passwordless", idToken) to get the long-lived session token.

Support for logging in with Facebook is implemented by the FacebookAuthFilter. This filter responds to requests at /facebook_auth.

The endpoint expects an appid value from the ‘state’ parameter, e.g. ?state={appid}. If that parameter is missing, the default (root) app will be used as authentication target.

To initiate a login with Facebook just redirect the user to the Facebook OAuth endpoint:

facebook.com/dialog/oauth

Pass the parameter redirect_uri=/facebook_auth so Para can handle the response from Facebook. For apps other than the root app use redirect_uri=/facebook_auth?state=myapp instead.

Note: You need to register a new application with Facebook in order to obtain an access and secret keys.

Below is an example Javascript code for a Facebook login button:

$("#facebookLoginBtn").click(function() {
        window.location = "https://www.facebook.com/dialog/oauth?" +
                "response_type=code&client_id={FACEBOOK_APP_ID}" +
                "&scope=email&state=" + APPID +
                "&redirect_uri=" + window.location.origin + "/facebook_auth";
        return false;
});

GitHub support

This describes the web authentication flow with GitHub. You could also login with an existing access token from GitHub through the API. This web flow sets a cookie, the API returns a JWT instead.

First of all you need to have your API credentials ready by creating an app on GitHub. Then add them to your application.conf configuration file:

para.gh_app_id = "..."
para.gh_secret = "..."

Or add these through the app settings API:

{
    "gh_app_id": "..."
    "gh_secret": "..."
    "signin_success": "http://success.url"
    "signin_failure": "http://failure.url"
}

Para can return a short-lived ID token back to the client which initiated the request. Add jwt=id to your signin_success url. For example { "signin_success": "http://success.url?jwt=id" }. The id value of the parameter will be replaced with the actual JWT ID token, which you can use to call paraClient.signIn("passwordless", idToken) to get the long-lived session token.

Support for logging in with GitHub is implemented by the GitHubAuthFilter. This filter responds to requests at /github_auth.

The endpoint expects an appid value from the ‘state’ parameter, e.g. ?state={appid}. If that parameter is missing, the default (root) app will be used as authentication target.

To initiate a login with GitHub just redirect the user to the GitHub OAuth endpoint:

github.com/login/oauth/authorize

Pass the parameter redirect_uri=/github_auth so Para can handle the response from GitHub. For apps other than the root app use redirect_uri=/github_auth?state=myapp instead.

Note: You need to register a new application with GitHub in order to obtain an access and secret keys.

Below is an example Javascript code for a GitHub login button:

$("#githubLoginBtn").click(function() {
        window.location = "https://github.com/login/oauth/authorize?" +
                "response_type=code&client_id={GITHUB_APP_ID}" +
                "&scope=user&state=" + APPID +
                "&redirect_uri=" + window.location.origin + "/github_auth";
        return false;
});

Google support

This describes the web authentication flow with Google. You could also login with an existing access token from Google through the API. This web flow sets a cookie, the API returns a JWT instead.

First of all you need to have your API credentials ready by creating an app in the Google Dev Console. Then add them to your application.conf configuration file:

para.gp_app_id = "..."
para.gp_secret = "..."

Or add these through the app settings API:

{
    "gp_app_id": "..."
    "gp_secret": "..."
    "signin_success": "http://success.url"
    "signin_failure": "http://failure.url"
}

Para can return a short-lived ID token back to the client which initiated the request. Add jwt=id to your signin_success url. For example { "signin_success": "http://success.url?jwt=id" }. The id value of the parameter will be replaced with the actual JWT ID token, which you can use to call paraClient.signIn("passwordless", idToken) to get the long-lived session token.

Support for logging in with Google acoounts is implemented by the GoogleAuthFilter. This filter responds to requests at /google_auth.

The endpoint expects an appid value from the ‘state’ parameter, e.g. ?state={appid}. If that parameter is missing, the default (root) app will be used as authentication target.

To initiate a login with Google just redirect the user to the Google OAuth endpoint:

accounts.google.com/o/oauth2/v2/auth

Pass the parameter redirect_uri=/google_auth so Para can handle the response from Google. For apps other than the root app use redirect_uri=/google_auth?state=myapp instead.

Note: You need to register a new application with Google in order to obtain an access and secret keys.

Below is an example Javascript code for a Google login button:

$("#googleLoginBtn").click(function() {
        var baseUrl = window.location.origin;
        window.location = "https://accounts.google.com/o/oauth2/v2/auth?" +
                "client_id={GOOGLE_APP_ID}&response_type=code" +
                "&scope=openid%20email&redirect_uri=" + baseUrl + "/google_auth" +
                "&state=" + APPID + "&" + "openid.realm=" + baseUrl;
        return false;
});

LinkedIn support

This describes the web authentication flow with LinkedIn. You could also login with an existing access token from LinkedIn through the API. This web flow sets a cookie, the API returns a JWT instead.

First of all you need to have your API credentials ready by creating an app on LinkedIn. Then add them to your application.conf configuration file:

para.in_app_id = "..."
para.in_secret = "..."

Or add these through the app settings API:

{
    "in_app_id": "..."
    "in_secret": "..."
    "signin_success": "http://success.url"
    "signin_failure": "http://failure.url"
}

Para can return a short-lived ID token back to the client which initiated the request. Add jwt=id to your signin_success url. For example { "signin_success": "http://success.url?jwt=id" }. The id value of the parameter will be replaced with the actual JWT ID token, which you can use to call paraClient.signIn("passwordless", idToken) to get the long-lived session token.

Support for logging in with LinkedIn is implemented by the LinkedInAuthFilter. This filter responds to requests at /linkedin_auth.

The endpoint expects an appid value from the ‘state’ parameter, e.g. ?state={appid}. If that parameter is missing, the default (root) app will be used as authentication target.

To initiate a login with LinkedIn just redirect the user to the LinkedIn OAuth endpoint:

www.linkedin.com/oauth/v2/authorization

Pass the parameter redirect_uri=/linkedin_auth so Para can handle the response from LinkedIn. For apps other than the root app use redirect_uri=/linkedin_auth?state=myapp instead.

Note: You need to register a new application with LinkedIn in order to obtain an access and secret keys.

Below is an example Javascript code for a LinkedIn login button:

$("#linkedinLoginBtn").click(function() {
        window.location = "https://www.linkedin.com/oauth/v2/authorization?" +
                "response_type=code&client_id={LINKEDIN_APP_ID}" +
                "&scope=r_liteprofile%20r_emailaddress&state=" + APPID +
                "&redirect_uri=" + window.location.origin + "/linkedin_auth";
        return false;
});

Microsoft support

This describes the web authentication flow with Microsoft. You could also login with an existing access token from Microsoft through the API. This web flow sets a cookie, the API returns a JWT instead.

First of all you need to have your API credentials ready by creating an app on Microsoft. Then add them to your application.conf configuration file:

para.ms_app_id = "..."
para.ms_secret = "..."
para.security.signin_success = "http://success.url"
para.security.signin_failure = "http://failure.url"

Or add these through the app settings API:

{
    "ms_app_id": "..."
    "ms_secret": "..."
    "signin_success": "http://success.url"
    "signin_failure": "http://failure.url"
}

Para can return a short-lived ID token back to the client which initiated the request. Add jwt=id to your signin_success url. For example { "signin_success": "http://success.url?jwt=id" }. The id value of the parameter will be replaced with the actual JWT ID token, which you can use to call paraClient.signIn("passwordless", idToken) to get the long-lived session token.

Support for logging in with Microsoft accounts is implemented by the MicrosoftAuthFilter. This filter responds to requests at /microsoft_auth.

The endpoint expects an appid value from the ‘state’ parameter, e.g. ?state={appid}. If that parameter is missing, the default (root) app will be used as authentication target.

To initiate a login with Microsoft just redirect the user to the Microsoft OAuth endpoint:

login.microsoftonline.com/common/oauth2/v2.0/authorize

Pass the parameter redirect_uri=/microsoft_auth?state=myapp so Para can handle the response from Microsoft.

Note: You need to register a new application with Microsoft in order to obtain an access and secret keys.

Below is an example Javascript code for a Microsoft login button:

$("#microsoftLoginBtn").click(function() {
        window.location = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?" +
                "response_type=code&client_id={MICROSOFT_APP_ID}" +
                "&scope=https%3A%2F%2Fgraph.microsoft.com%2Fuser.read&state=" + APPID +
                "&redirect_uri=" + window.location.origin + "/microsoft_auth;
        return false;
});

Slack support

This describes the web authentication flow with Slack. You could also login with an existing access token from Slack through the API. This web flow sets a cookie, the API returns a JWT instead.

First of all you need to have your API credentials ready by creating an app on Slack. Then add them to your application.conf configuration file:

para.sl_app_id = "..."
para.sl_secret = "..."
para.security.signin_success = "http://success.url"
para.security.signin_failure = "http://failure.url"

Or add these through the app settings API:

{
    "sl_app_id": "..."
    "sl_secret": "..."
    "signin_success": "http://success.url"
    "signin_failure": "http://failure.url"
}

Para can return a short-lived ID token back to the client which initiated the request. Add jwt=id to your signin_success url. For example { "signin_success": "http://success.url?jwt=id" }. The id value of the parameter will be replaced with the actual JWT ID token, which you can use to call paraClient.signIn("passwordless", idToken) to get the long-lived session token.

Support for logging in with Slack accounts is implemented by the SlackAuthFilter. This filter responds to requests at /slack_auth.

The endpoint expects an appid value from the ‘state’ parameter, e.g. ?state={appid}. If that parameter is missing, the default (root) app will be used as authentication target.

To initiate a login with Slack just redirect the user to the Slack OAuth endpoint:

slack.com/oauth/authorize

Pass the parameter redirect_uri=/slack_auth?state=myapp so Para can handle the response from Slack.

Note: You need to register a new application with Slack in order to obtain an access and secret keys.

Below is an example Javascript code for a Slack login button:

$("#slackLoginBtn").click(function() {
        window.location = "https://slack.com/oauth/authorize?" +
                "response_type=code&client_id={SLACK_APP_ID}" +
                "&scope=identity.basic%20identity.email%20identity.team%20identity.avatar&state=" + APPID +
                "&redirect_uri=" + window.location.origin + "/slack_auth;
        return false;
});

Twitter support

This describes the web authentication flow with Twitter. You could also login with an existing access token from Twitter through the API. This web flow sets a cookie, the API returns a JWT instead.

First of all you need to have your API credentials ready by creating an app on Twitter. Then add them to your application.conf configuration file:

para.tw_app_id = "..."
para.tw_secret = "..."

Or add these through the app settings API:

{
    "tw_app_id": "..."
    "tw_secret": "..."
    "signin_success": "http://success.url"
    "signin_failure": "http://failure.url"
}

Para can return a short-lived ID token back to the client which initiated the request. Add jwt=id to your signin_success url. For example { "signin_success": "http://success.url?jwt=id" }. The id value of the parameter will be replaced with the actual JWT ID token, which you can use to call paraClient.signIn("passwordless", idToken) to get the long-lived session token.

Support for logging in with Twitter is implemented by the TwitterAuthFilter. This filter responds to requests at /twitter_auth.

The endpoint expects an appid value from the ‘state’ parameter, e.g. ?state={appid}. If that parameter is missing, the default (root) app will be used as authentication target.

To initiate a login with Twitter just redirect the user to the /twitter_auth. This will redirect the user to Twitter for authentication. For apps other than the root app, redirect to /twitter_auth?state=myapp.

Note: You need to register a new application with Twitter in order to obtain an access and secret keys. The Twitter API does not share users’ emails by default, you have to ask Twitter to whitelist your app first.

Below is an example Javascript code for a Twitter login button:

$("#twitterLoginBtn").click(function() {
        window.location = "/twitter_auth";
        // for apps other than the root app use:
        // window.location = "/twitter_auth?state=" + APPID;
        return false;
});

Amazon support

This describes the web authentication flow with Amazon. You could also login with an existing access token from Amazon through the API. This web flow sets a cookie, the API returns a JWT instead.

First of all you need to have your API credentials ready by creating an app on Amazon. Then add them to your application.conf configuration file:

para.az_app_id = "..."
para.az_secret = "..."
para.security.signin_success = "http://success.url"
para.security.signin_failure = "http://failure.url"

Or add these through the app settings API:

{
    "az_app_id": "..."
    "az_secret": "..."
    "signin_success": "http://success.url"
    "signin_failure": "http://failure.url"
}

Para can return a short-lived ID token back to the client which initiated the request. Add jwt=id to your signin_success url. For example { "signin_success": "http://success.url?jwt=id" }. The id value of the parameter will be replaced with the actual JWT ID token, which you can use to call paraClient.signIn("passwordless", idToken) to get the long-lived session token.

Support for logging in with Amazon accounts is implemented by the AmazonAuthFilter. This filter responds to requests at /amazon_auth.

The endpoint expects an appid value from the ‘state’ parameter, e.g. ?state={appid}. If that parameter is missing, the default (root) app will be used as authentication target.

To initiate a login with Amazon just redirect the user to the Amazon OAuth endpoint:

www.amazon.com/ap/oa

Pass the parameter redirect_uri=/amazon_auth?state=myapp so Para can handle the response from Amazon.

Note: You need to register a new application with Amazon in order to obtain an access and secret keys.

Below is an example Javascript code for a Amazon login button:

$("#amazonLoginBtn").click(function() {
        window.location = "https://www.amazon.com/ap/oa?" +
                "response_type=code&client_id=" + AMAZON_APP_ID +
                "&scope=profile&state=" + APPID +
                "&redirect_uri=" + ENDPOINT + "/amazon_auth";
        return false;
});

Groups and roles

Para defines three security groups and roles:

  • users group with role user (normal users)
  • mods group with role mod (moderators)
  • admins group with role admin (administrators)

There is also a special role app for applications.

CSRF protection

Cross-Site Request Forgery protection is enabled by default for all POST, PUT and DELETE requests. It works by verifying that incoming requests contain a security token (CSRF token) and it matches the one stored on the server. CSRF protection is disabled for API requests.

To disable the CSRF protection set para.security.csrf_protection to false. You can also tell Para to send a cookie containing the CSRF token by setting para.security.csrf_cookie as the cookie name. Leaving this parameter blank will disable the CSRF cookie.

For example, if you wish to integrate CSRF protection with AngularJS you can do so by setting the cookie name to XSRF-TOKEN which is the default cookie used by AngularJS. Also you need to configure AngularJS to send the correct CSRF header to Para which is X-CSRF-TOKEN like so:

$httpProvider.defaults.xsrfHeaderName = "X-CSRF-TOKEN";

In order to get a CSRF token from the server you need to send a POST request to a protected resource like POST /protected/ping and the server will return the token as cookie.

Authentication

Para uses the AWS Signature Version 4 algorithm for signing API requests. We chose this algorithm instead of OAuth because it is less complicated and is already implemented inside the core AWS Java SDK, which we have as direct dependency. In terms of security, both algorithms are considered very secure so there’s no compromise in that aspect.

Para offers two ways of authentication - one for apps using API keys and one for insecure clients (mobile, JS) using JWT. Apps authenticated with a secret key have full access to the API. Users authenticated with social login are issued JWT tokens and have limited access to the API, for example they can’t generate new API keys and they are authorized by specific resource permissions (see Resource permissions).

Full access for apps

In order to make a request to the API you need to have a pair of access and secret keys. Access keys are part of the HTTP request and secret keys are used for signing only and must be kept safe.

We recommend that you choose one of our API client libraries to handle the authentication for you.

First-time setup

Call GET /v1/_setup to get your first key pair. Once you do this you will get back a response like:

{
    "secretKey": "U3VTNifLPqnZ1W2S3pVVuKG4HOVbimMocdDMl8T69BB001AXGZtwZw==",
    "accessKey": "YXBwOnBhcmE=",
    "info":        "Save the secret key! It is showed only once!"
}

Make sure you save these security credentials because the API can only be accessed with them. Once you have the keys you can start making signed requests to the API. Also you can use these keys to create applications which will have their own separate keys (see Apps).

Note: when a resource has public permissions you can access it without setting the Authorization header. Simply specify your access key as a parameter:

GET /v1/public/resource?accessKey=app:myapp

Changing keys

Call POST /v1/_newkeys to generate a new secret key (the request must be signed with the old keys).

JSON Web Tokens - client access based on permissions

Important: Access to the root app is disabled for API clients holding a bearer JSON web token.

This is not the case for clients that use an accessKey and a secretKey, only those that use access tokens (JWT).

Para apps can create new users and grant them specific permissions by implementing social login (identity federation). First a user authenticates with their social identity provider such as Facebook, then comes back to Para with the access_token and is issued a new JSON Web Token that allows him to access the REST API.

JWT tokens are a new standard for authentication which is similar to cookies but is more secure, compact and stateless. An encoded token looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG
4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

When decoded the token looks like this:

// HEADER:
{
  "alg": "HS256",
  "typ": "JWT"
}
// PAYLOAD:
{
  "sub": "587408205806571520",
  "appid": "app:para",
    "refresh": 1450137214490,
  "nbf": 1450133614,
    "exp": 1450738414,
    "iat": 1450133614
}

To authenticate with users with social login use the Para client:

paraClient.signIn(String provider, String providerToken);

Supported providers are facebook, google, twitter, github, linkedin, microsoft, slack, password, oauth2, oauth2second, oauth2third, ldap, passwordless.

You will have to add the API credentials for each of these in your application.conf configuration file:

# facebook
para.fb_app_id = "..."
para.fb_secret = "..."
# google
para.gp_app_id = "..."
para.gp_secret = "..."
# linkedin
para.in_app_id = "..."
para.in_secret = "..."
# twitter
para.tw_app_id = "..."
para.tw_secret = "..."
# github
para.gh_app_id = "..."
para.gh_secret = "..."
# microsoft
para.ms_app_id = "..."
para.ms_secret = "..."
# slack
para.sl_app_id = "..."
para.sl_secret = "..."
# generic oauth2
para.oa2_app_id = "..."
para.oa2_secret = "..."
# second generic oauth2
para.oa2second_app_id = "..."
para.oa2second_secret = "..."
# third generic oauth2
para.oa2third_app_id = "..."
para.oa2third_secret = "..."

For example calling paraClient.signIn("facebook", "facebook_access_token") should return a new User object and would store the JWT token in memory. To get an access token from Facebook, use their JavaScript SDK.

After you call paraClient.signIn() and the request succeeds, the client caches the access token and all subsequent requests to the API will include that token until paraClient.signOut() is called. This is different from the normal operation using access and secret keys. Usually, tokens are used to authenticate users, unless they are “super” tokens, which can authenticate apps (see below).

To get or set access tokens use:

paraClient.getAccessToken();
paraClient.setAccessToken(String token);

To sign out and clear the JWT access token use:

paraClient.signOut();

Tokens can also be revoked by calling paraClient.revokeAllTokens() but this only works for authenticated users. The Para client takes care of refreshing the JWT tokens every hour and by default all tokens are valid for a week.

Creating “super” tokens

Since v1.18.3 we’ve added the support for “super” JSON web tokens. These are just like normal tokens for users, but instead of authenticating users we can authenticate apps with them. Such tokens give clients full access to the API. Super tokens can also be generated on the client-side but your code will need both the Para access key and secret key. Be careful not to expose your Para secret key when using this method.

For example, lets assume we have some JavaScript app running in the browser and we need admin access to our Para app. We could use the JavaScript client for Para but putting the secret key inside client-side code on the browser is not a smart move. So we pull in a library for generating JWT, like jsrsasign and we create the token ourselves. Here’s a snippet:

function getJWT(appid, secret) {
    var now = Math.round(new Date().getTime() / 1000);
    var sClaim = JSON.stringify({
        exp: now + (7 * 24 * 60 * 60), // expires at
        iat: now, // issued at
        nbf: now, // not valid before
        appid: appid // app id must be present
    });
    var sHeader = JSON.stringify({'alg': 'HS256', 'typ': 'JWT'});
    return KJUR.jws.JWS.sign(null, sHeader, sClaim, secret);
}

Your JS app could ask the user for the access keys, create a JWT and then discard the keys and use the newly generated “super” token. Once we have this we can attach it to every request as a header:

Authorization: Bearer eyJhbGciOiVCJ9.eyJzdWIiOi0._MKAH6SGiKSoqgwmqUaxuMyE

When calling the Para API from JavaScript in the browser, make sure you are running a web server and not as file:/// or your browser might not allow CORS requests. Also check that CORS is enabled in Para with para.cors_enabled = true.

Calling the API from Postman

You can access the protected API from Postman since it has support for the AWS Signature v4 algorithm, used in Para. Open up Postman and in the “Authorization” tab choose “AWS Signature”:

Access Key

Your Para access key, e.g. app:myapp.

Secret Key

Your Para secret key.

Region

Not used. It should always be us-east-1.

Service Name

This should always be para.

Make sure you don’t have extra headers in your requests because Postman is known to calculate invalid signatures for such requests.

API methods

All API methods below require authentication by default, unless it’s written otherwise.

Limiting which fields are returned by the API

Field limiting is supported on all requests by using the query parameter select=xxx,yyy. This parameter takes a comma separated list of fields to include. For example:

GET /myobjects?select=id,name,my_field1,my_field2



▾▴ collapse/expand all

POST /jwt_auth

Takes an identity provider access token and fetches the user data from that provider. A new User object is created if that user doesn’t exist and is then returned. Access tokens are returned upon successful authentication using one of the SDKs from Facebook, Google, Twitter, etc.

Note: Twitter uses OAuth 1 and gives you a token and a token secret so you must concatenate them first - {oauth_token}:{oauth_token_secret}, and then use that as the provider access token. Also if you use the password provider, the token parameter must be in the format {email}:{full_name}:{password} or {email}::{password} (must be :: if name is empty). For LDAP the token looks similar - {uid}:{password} (single :).

Also keep in mind that when a new user signs in with a password and unverified email, through /jwt_auth, Para will create the user but will return an error 400 indicating that the user is not active and cannot be authenticated. Once the email is verified and the user is set to active: true, subsequent sign in attempts will be successful.

Request

  • body - a JSON object containing appid, provider and token properties (required).

Request body example for authenticating with email and password:

{
    "appid": "app:myapp",
    "provider": "password",
    "token": "[email protected]::password123"
}

Request body example for Facebook:

{
    "appid": "app:myapp",
    "provider": "facebook",
    "token": "eyJhbGciOiJIUzI1NiJ9.eWIiO..."
}

The appid is the id of your own app that you’re trying to sign in to. The provider field a string and can be one of the following values:

  • facebook - sign in with Facebook account,
  • google - sign in with Google account,
  • twitter - sign in with Twitter account,
  • github - sign in with GitHub account,
  • linkedin - sign in with LinkedIn account,
  • microsoft - sign in with Microsoft account,
  • slack - sign in with Slack account,
  • amazon - sign in with Amazon account,
  • password - sign in with email and password.
  • oauth2 - sign in with generic OAuth 2.
  • oauth2second - sign in with “second” generic OAuth 2 provider.
  • oauth2third - sign in with “third” generic OAuth 2 provider.
  • ldap - sign in with LDAP.
  • passwordless - sign in with an external authentication provider (custom SSO)

Response

Returns a JSON object containing JWT properties and a User object. The returned JWT properties are:

  • access_token - the JWT access token.

  • expires - a Java timestamp of when the token will expire.

  • refresh - a Java timestamp indicating when API clients should refresh their tokens, usually 1 hour after token has been issued.

  • status codes - 200, 400

Example response:

{
    "jwt": {
        "access_token": "eyJhbGciOiJIUzI1NiJ9.eyJ...",
        "expires": 1450137214490,
        "refresh": 1450137216490
    },
    "user": {
        "id":"user1",
        "timestamp": 1399721289987,
        "type":"user",
        "appid":"myapp",
        ...
    }
}

GET /jwt_auth

Refreshes the access token if and only if the provided token is valid and not expired. Tokens should be refreshed periodically in order to keep users logged in for longer periods of time.

Request

Request should include an Authorization: Bearer {JWT_TOKEN} header containing a valid access token. (required) As an alternative you could provide the token as query parameter instead of a header.

Response

Returns a JSON object containing a new JWT token and the same User object. The returned JWT properties are:

  • access_token - the JWT access token.

  • expires - a Java timestamp of when the token will expire.

  • refresh - a Java timestamp indicating when API clients should refresh their tokens.

  • status codes - 200, 400

Example response:

{
    "jwt": {
        "access_token": "eyJhbGciOiJIUzI1NiJ9.eyJ...",
        "expires": 1450137214490,
        "refresh": 1450137216490
    },
    "user": {
        "id":"user1",
        "timestamp": 1399721289987,
        "type":"user",
        "appid":"myapp",
        ...
    }
}

DELETE /jwt_auth

Revokes all user tokens for the user that is currently logged in. This would be equivalent to “logout everywhere”. Note: Generating a new API secret on the server will also invalidate all client tokens.

Request

Request should include an Authorization: Bearer {JWT_TOKEN} header containing a valid access token. (required) As an alternative you could provide the token as query parameter instead of a header.

Response

Returns a JSON object containing a new JWT token and the same User object. The returned JWT properties are:

  • access_token - the JWT access token.

  • expires - a Java timestamp of when the token will expire.

  • refresh - a Java timestamp indicating when API clients should refresh their tokens.

  • status codes - 200, 400

Example response (response is empty):

200 OK

POST /v1/{type}

Creates a new object of type {type}. You can also create objects with custom types and fields (since v1.4.0).

Request

  • body - the JSON object to create
  • {type} - the plural form of the object’s type, e.g. “users”

Example request body:

{
    "type":"tag",
    "plural":"tags",
    "tag":"tag1"
}

Notice how the type field is in singular form and the plural field is the plural form of the type’s name. These are required for mapping types to URLs.

Response

  • status codes - 201, 400

Example response for a Tag:

{
    "id":"tag:tag1",
    "timestamp":1399721289987,
    "type":"tag",
    "appid":"para",
    "name":"tag tag:tag1",
    "votes":0,
    "plural":"tags",
    "objectURI":"/tags/tag1",
    "tag":"tag1",
    "count":0
}

GET /v1/{type}/{id}

Returns an object of type {type}.

Request

  • {type} - the plural form of the object’s type, e.g. “users”
  • {id} - the id

Note: If {id} is omitted then the response will be a list of all objects of the specified type.

Response

  • status codes - 200, 404

Example response for a Tag:

{
    "id":"tag:tag1",
    "timestamp":1399721289987,
    "type":"tag",
    "appid":"para",
    "name":"tag tag:tag1",
    "votes":0,
    "plural":"tags",
    "objectURI":"/tags/tag1",
    "tag":"tag1",
    "count":0
}

PUT /v1/{type}/{id}

Overwrites an object of type {type}. If the object with this {id} doesn’t exist then a new one will be created.

Request

  • body - the JSON data to merge with the stored object
  • {type} - the plural form of the object’s type, e.g. “users”
  • {id} - the id

Response

  • status codes - 200, 400, 404, 500

Example response for a Tag with updated count:

{
    "id":"tag:tag1",
    "timestamp":1399721289987,
    "type":"tag",
    "appid":"para",
    "name":"tag tag:tag1",
    "votes":0,
    "plural":"tags",
    "objectURI":"/tags/tag1",
    "tag":"tag1",
    "count":55
}

PATCH /v1/{type}/{id}

Updates an existing object of type {type}. Partial objects are supported, meaning that only a few fields could be updated, without having to send the whole object.

Vote requests: these are a special kind of PATCH request, which has a body like {"_voteup": "user123"} or {"_votedown": "user123"}. Here user123 is the id of the voter. A successful vote request either increments or decrements the votes field by 1.

Request

  • body - the JSON object to merge with the stored object OR a vote request body like {"_voteup": "user123"}
  • {type} - the plural form of the object’s type, e.g. “users”
  • {id} - the id

A vote request body can look like this:

{
    "_voteup": "obj123",
    "_vote_locked_after": 60,
    "_vote_expires_after": 2592000
}

Response

  • status codes - 200, 400, 404, 412, 500, vote requests return true or false

If optimistic locking is enabled and the DAO implementation supports it, failed updates will result in 412 Precondition Failed.

Example response for a Tag with updated count:

{
    "id":"tag:tag1",
    "timestamp":1399721289987,
    "type":"tag",
    "appid":"para",
    "name":"tag tag:tag1",
    "votes":0,
    "plural":"tags",
    "objectURI":"/tags/tag1",
    "tag":"tag1",
    "count":55
}

DELETE /v1/{type}/{id}

Deletes an existing object of type {type}. Returns code 200 regardless of the success of the request.

Request

  • {type} - the plural form of the object’s type, e.g. “users”
  • {id} - the id

Response

  • status codes - 200, 400

No content.

POST /v1/_batch

Creates multiple objects with a single request. PUT requests to this resource are equivalent to POST.

Request

  • body - a JSON array of objects to create (required).

Maximum request size is 1 megabyte.

Response

  • status codes - 200, 400

Example response for creating 3 objects (returns a list of the created objects):

[ { "id":"id1", ... }, { "id":"id2", ... }, { "id":"id3", ... } ]

GET /v1/_batch

Returns a list of objects given their id fields.

Parameters

  • ids - a list of ids of existing objects (required).

Example: GET /v1/_batch?ids=id1&ids=id2&ids=id3

Response

  • status codes - 200, 400 (if no ids are specified)

Example response for reading 3 objects:

[ { "id":"id1", ... }, { "id":"id2", ... }, { "id":"id3", ... } ]

PATCH /v1/_batch

Updates multiple objects with a single request. Partial objects are supported. Note: These objects will not be validated as this would require us to read them first and validate them one by one.

Request

  • body - a JSON array of objects to update (required). The fields id and type are required for each object.

Maximum request size is 1 megabyte.

Response

  • status codes - 200, 400, 412

If optimistic locking is enabled and the DAO implementation supports it, failed updates will be ignored and omitted from the response array. Error 412 is returned only if all object failed to update due to version locking.

Example response for updating 3 objects (returns a list of the updated objects):

[ {
    "id":"id1",
    "type":"type1",
    "name":"newName1", ...
  }, {
    "id":"id2",
    "type":"type2",
    "name":"newName2", ...
  }, {
     "id":"id3",
     "type":"type3",
     "name":"newName3", ...
} ]

DELETE /v1/_batch

Deletes multiple objects with a single request.

Parameters

  • ids - a list of ids of existing objects (required).

Example: DELETE /v1/_batch?ids=1&ids=2 will delete the two objects with an id of 1 and 2, respectively.

Response

  • status codes - 200, 400 (if request maximum number of ids is over the limit of ~30)

No content.

GET /v1/{type}

Searches for objects of type {type}.

Note: Requests to this path and /v1/{type}/search/{querytype} are handled identically. Also, note that custom fields must be used in search queries as properties.myfield.

For detailed syntax of the query string see Lucene’s query string syntax.

Request

  • {type} - the plural form of the object’s type, e.g. “users”
  • {querytype} - the type of query to execute (optional, see Search)

Parameters

  • q - a search query string (optional). Defaults to * (all).
  • desc - sort order - true for descending (optional). Default is true.
  • sort - the field to sort by (optional).
  • limit - the number of results to return. Default is 30.
  • page - starting page for results (optional). (note: page size is 30 items by default)

Response

  • status codes - 200, 400 if query string syntax is invalid

Example response for querying all tags GET /v1/tags?q=*&limit=3:

{
    "page":0,
    "totalHits":3,
    "items":[{
        "id":"tag:tag3",
        "timestamp":1400077389250,
        "type":"tag",
        "appid":"para",
        "name":"tag tag:tag3",
        "votes":0,
        "plural":"tags",
        "objectURI":"/tags/tag3",
        "tag":"tag3",
        "count":0
    }, {
        "id":"tag:tag1",
        "timestamp":1400077383588,
        "type":"tag",
        "appid":"para",
        "name":"tag tag:tag1",
        "votes":0,
        "plural":"tags",
        "objectURI":"/tags/tag1",
        "tag":"tag1",
        "count":0
    }, {
        "id":"tag:tag2",
        "timestamp":1400077386726,
        "type":"tag",
        "appid":"para",
        "name":"tag tag:tag2",
        "votes":0,
        "plural":"tags",
        "objectURI":"/tags/tag2",
        "tag":"tag2",
        "count":0
    }]
}

GET /v1/search/{querytype}

Executes a search query.

Note: custom fields must be used in search queries as properties.myfield.

Request

  • {querytype} - the type of query to execute (optional), use one of types below:

The querytype parameter switches between the different query types. If this parameter is missing then the generic findQuery() method will be executed by default.

id query

Finds the object with the given id from the index or null. This executes the method findById() with these parameters:

  • id - the id to search for

ids query

Finds all objects matchings the given ids. This executes the method findByIds() with these parameters:

  • ids - a list of ids to search for

nested query

Searches through objects in a nested field named nstd. Used internally for joining search queries on linked objects. This executes the method findNestedQuery() with these parameters:

  • q - a search query string
  • field - the name of the field to target within a nested object
  • type - a type to search for

nearby query

Location-based search query. Relies on Address objects for coordinates. This executes the method findNearby() with these parameters:

  • latlng - latitude and longitude of the center of the search perimeter
  • radius - radius of the search perimeter in kilometers.

prefix query

Searches for objects containing a field (property) that starts with the given prefix. This executes the method findPrefix() with these parameters:

  • field - the field to search on
  • prefix - the prefix

similar query

“More like this” search query. This executes the method findSimilar() with these parameters:

  • fields - a list of fields, for example:
    GET /v1/search/similar?fields=field1&fields=field2
  • filterid - an id filter; excludes a particular object from the results
  • like - the source text to use for comparison. If the like parameter starts with id:, e.g. id:123, then the source of the “like” text is read from the object with id 123 and extracted from fields fields on the server. This operation can be done on the server in situations where the supplied text in like is too long and causes “URI too long” exceptions.

tagged query

Search for objects tagged with a set of tags. This executes the method findTagged() with these parameters:

  • tags - a list of tags, for example:
    GET /v1/search/tagged?tags=tag1&tags=tag2

in query

Searches for objects containing any of the terms in the given list (matched exactly). This executes the method findTermInList() with these parameters:

  • field - the field to search on
  • terms - a list of terms (values)

terms query

Searches for objects containing all of the specified terms (matched exactly) This executes the method findTerms() with these parameters:

  • matchall - if true executes an AND query, otherwise an OR query
  • terms - a list of field:term pairs, for example:
    GET /v1/search/terms?terms=field1:term1&terms=field2:term2
  • count - if present will return 0 objects but the “totalHits” field will contain the total number of results found that match the given terms.

Since v1.9, the terms query supports ranges. For example if you have a pair like 'age':25 and you want to find objects with higher age value, you can modify the key to have a relational operator 'age >':25. You can use the >, <, >=, <= operators by appending them to the keys of the terms map.


wildcard query

A wildcard query like “example*“. This executes the method findWildcard() with these parameters:

  • field - the field to search on

count query

Returns the total number of results that would be returned by a query. This executes the method getCount() and has no additional parameters.

Request parameters

  • q - a search query string (optional). Defaults to * (all).
  • type - the type of objects to search for (optional).
  • desc - sort order - true for descending (optional). Default is true.
  • sort - the field to sort by (optional).
  • page - starting page for results (optional). (note: page size is 30 items by default)
  • limit - the number of results to return

Response

  • status codes - 200, 400 if query string syntax is invalid

Example response for counting all objects (just three for this example):

{
    "page":0,
    "totalHits":3,
    "items":[
    ]
}

GET /v1/{type}/{id}/links/{type2}/{id2}

Call this method to search for objects that linked to the object with the given {id}.

Note: When called with the parameter ?childrenonly, the request is treated as a “one-to-many” search request. It will do asearch for child objects directly connected to their parent by the parentid field. Without ?childrenonly the request is treated as a “many-to-many” search request.

Request

  • {type} - the type of the first object, e.g. “users” (required)
  • {id} - the id of the first object (required)
  • {type2} - the type of the second object (required)
  • {id2} - the id field of the second object (optional)

Parameters

  • childrenonly - if set and {id2} is not set, will return a list of child objects (these are the objects with parentid equal to {id} above). Also if field and term parameters are set, the results are filtered by the specified field and the value of that field (term).
  • count - if set will return no items an the total number of linked objects. If childrenonly is set, this will return only the count of child objects.
  • q - query string, if set, all linked/child objects will be searched and those that match the query are returned. To search only child objects that are linked by parentid use q in combination with childrenonly, otherwise the Linker objects will be searched. Since v1.19 Linker objects contain a copy of the two objects they connect. This enables Para to execute more complex “joined” search queries. The effectiveness of these is determined by how up-to-date the data inside a Linker is.

Response

  • If the {id2} parameter is specified, the response will be a boolean text value - true if objects are linked.

  • If the {id2} parameter is missing, the response will be a list of linked objects. (pagination parameters are applicable)

  • childrenonly - if set, the response will be a list of child objects (pagination parameters are applicable)

  • status codes - 200, 400 (if type parameter is missing)

Example response if id2 is missing:

{
    "page":X,
    "totalHits":Y,
    "items":[
        ...
    ]
}

Response if id2 is specified: true or false

POST /v1/{type}/{id}/links/{id2}

This will link the object with {id} to another object with the specified id in the id parameter. The created link represents a many-to-many relationship (see also one-to-many relationships).

Don’t use this method for “one-to-many” links. Creating one-to-many links is trivial - just set the parentid of an object (child) to be equal to the id field of another object (parent).

PUT requests to this resource are equivalent to POST.

Request

  • {type} - the type of the first object, e.g. “users”
  • {id} - the id of the first object
  • {id2} - the id field of the second object (required)

Response

Returns the id of the Linker object - the linkId - which contains the types and ids of the two objects.

  • status codes - 200, 400 (if any of the parameters are missing)

Example response:

"type1:id1:type2:id2"

DELETE /v1/{type}/{id}/links/{type2}/{id2}

Unlinks or deletes the objects linked to the object with the specified {id}.

Request

  • {type} - the type of the first object, e.g. “users”
  • {id} - the id of the first object
  • {type2} - the type of the second object (not required, if this and {id2} are missing, it will unlink everything)
  • {id2} - the id field of the second object (optional)

Parameters

  • all - setting this will delete all linked objects (be careful!)
  • childrenonly - if set, all child objects will be deleted rather than unlinked (be careful!)

Note:

  • If both {type2} and {id2} are not set, all linked objects will be unlinked from this one.
  • If id is set - the two objects are unlinked.
  • If all and id are not set, but childrenonly is set then the child objects with type type are deleted! (these are the objects with parentid equal to {id} above)

Response

  • status codes - 200

No content.

PUT /v1/_constraints/{type}/{field}/{cname}

Adds a new validation constraint to the list of constraints for the given field and type.

Request

  • body - the JSON payload of the constraint (see the table below)

  • {type} - the object type to which the constraint applies (required)

  • {field} - the name of the field to which the constraint applies (required)

  • {cname} - the constraint name (required), one of the following:

    Name Payload (example)

    required

    none

    email

    none

    false

    none

    true

    none

    past

    none

    present

    none

    url

    none

    min

    { "value": 123 }

    max

    { "value": 123 }

    size

    { "min": 123, "max": 456 }

    digits

    { "integer": 4, "fraction": 2 }

    pattern

    { "value": "^[a-zA-Z]+$" }

    Response

    Returns a JSON object containing the validation constraints for the given type.

    • status codes - 200, 400

    Example response:

    {
        "User" : {
            "identifier" : {
                "required" : {
                    "message" : "messages.required"
                }
            },
            "groups" : {
                "required" : {
                    "message" : "messages.required"
                }
            },
            "email" : {
                "required" : {
                    "message" : "messages.required"
                },
                "email" : {
                    "message" : "messages.email"
                }
            }
        },
        ...
    }

GET /v1/_constraints/{type}

Returns an object containing all validation constraints for all defined types in the current app. This information can be used to power client-side validation libraries like valdr.

Request

  • {type} - when supplied, returns only the constraints for this type (optional). If this parameter is omitted, all constraints for all types will be returned.

Response

Returns a JSON object with all validation constraints for a given type. The message field is a key that can be used to retrieve a localized message.

  • status codes - 200

Example response:

{
    "User" : {
        "identifier" : {
            "required" : {
                "message" : "messages.required"
            }
        },
        "groups" : {
            "required" : {
                "message" : "messages.required"
            }
        },
        "email" : {
            "required" : {
                "message" : "messages.required"
            },
            "email" : {
                "message" : "messages.email"
            }
        }
    },
    ...
}

DELETE /v1/_constraints/{type}/{field}/{cname}

Removes a validation constraint from the list of constraints for the given field and type.

Request

  • {type} - the object type to which the constraint applies (required)
  • {field} - the name of the field to which the constraint applies (required)
  • {cname} - the constraint name (required, see the table above)

Response

Returns a JSON object containing the validation constraints for the given type.

  • status codes - 200, 400

Example response:

{
    "User" : {
        "identifier" : {
            "required" : {
                "message" : "messages.required"
            }
        },
        "groups" : {
            "required" : {
                "message" : "messages.required"
            }
        },
        "email" : {
            "required" : {
                "message" : "messages.required"
            },
            "email" : {
                "message" : "messages.email"
            }
        }
    },
    ...
}

GET /v1/_health

This resource is publicly accessible without authentication.

Displays the health status of the server. Status is updated about once every minute.

Request

No parameters.

Response

  • status codes - 200, 500

Example responses:

{
  "message" : "healthy"
}
{
    "code": 500,
  "message" : "unhealthy"
}

GET /v1/_id/{id}

Returns the object for the given {id}.

Request

  • {id} - the id

Response

Returns a JSON object.

  • status codes - 200, 404

Example response:

{
    "id" : "417283630780387328",
  "timestamp" : 1409572755025,
  "type" : "user",
    "name" : "Gordon Freeman"
    ...
}

GET /v1/_me

Returns the currently authenticated User or App object. If the request is unauthenticated 401 error is returned.

Request

No parameters.

Response

Returns the JSON object for the authenticated User or App.

  • status codes - 200, 401

Example response:

{
    "id" : "417283630780387328",
  "timestamp" : 1409572755025,
  "type" : "user",
    "name" : "Gordon Freeman"
    ...
}

GET /v1/_types

Returns a list of all known types for this application, including core types and user-defined types. User-defined types are custom types which can be defined through the REST API and allow the users to call the standard CRUD methods on them as if they were defined as regular Para objects. See User-defined classes for more details.

Request

No parameters.

Response

Returns a list of all types that are defined for this application.

  • status codes - 200

Example response for querying all types:

[
    "addresses":"address",
    "apps":"app",
    "sysprops":"sysprop",
    "tags":"tag",
    "translations":"translation",
    "users":"user",
    "votes":"vote"
]

GET /v1/_permissions/{subjectid}/{resource}/{method}

This checks if a subject is allowed to execute a specific type of request on a resource.

There are several methods and flags which control which requests can go through. These are:

  • GET, POST, PUT, PATCH, DELETE - use these to allow a certain method explicitly
  • ? - use this to enable public (unauthenticated) access to a resource
  • - - use this to deny all access to a resource
  • * - wildcard, allow all request to go through
  • OWN - allow subject to only access objects they created

Request

  • {subjectid} - the subject/user id to grant permissions to, or wildcard *. (required)
  • {resource} - the resource path or object type (URL encoded), or wildcard *. (required)
  • {method} - an HTTP method or flag, listed above. (required)

Response

Returns a boolean plain text response - true or false.

  • status codes - 200, 400, 404

Example response:

true

GET /v1/_permissions/{subjectid}

Returns a permissions objects containing all permissions. If {subjectid} is provided, the returned object contains only the permissions for that subject.

Request

  • {subjectid} - the subject/user id (optional)

Response

Returns a JSON object containing the resource permissions for the given user.

  • status codes - 200, 400, 404

Example response:

{
  "*": {
        "*": ["GET"]
    },
  "user1": [],
  "user2": {
        "posts": ["GET", "POST"]
    },
  "user3": {
    "*": ["*"]
  }
}

PUT /v1/_permissions/{subjectid}/{resource}

Grants a set of permissions (allowed HTTP methods) to a subject for a given resource.

There are several methods and flags which control which requests can go through. These are:

  • GET, POST, PUT, PATCH, DELETE - use these to allow a certain method explicitly
  • ? - use this to enable public (unauthenticated) access to a resource
  • - - use this to deny all access to a resource
  • * - wildcard, allow all request to go through
  • OWN - allow subject to only access objects they created

Request

  • body - a JSON array of permitted HTTP methods/flags, listed above (required).
  • {subjectid} - the subject/user id to grant permissions to, or wildcard * (required)
  • {resource} - the resource path or object type (URL encoded), or wildcard *. For example posts corresponds to /v1/posts, posts%2F123 corresponds to /v1/posts/123 (required)

Response

Returns a JSON object containing the resource permissions for the given user.

  • status codes - 200, 400, 404

Example response:

{
  "user2": {
        "posts": ["GET", "POST"]
    }
}

DELETE /v1/_permissions/{subjectid}/{resource}

Revokes all permissions for a given subject and resource. If {resource} is not specified, revokes every permission that has been granted to that subject.

Request

  • {subjectid} - the subject/user id to grant permissions to (required)
  • {resource} - the resource path or object type (URL encoded). If omitted,
  • all permissions* for that subject will be revoked. (optional)

Response

Returns a JSON object containing the resource permissions for the given user.

  • status codes - 200, 400, 404

Example response:

{
  "user1": [],
}

GET /v1/_settings

Lists all custom app settings. These can be user-defined key-value pairs and are stored withing the app object.

Request

No parameters.

Response

Returns an map of keys and values.

  • status codes - 200

Example response:

{
    "fb_app_id": "123U3VTNifLPqnZ1W2",
    "fb_secret": "YXBwOnBhcmE11234151667",
    "signin_success": "/dashboard",
    "signin_failure": "/signin?error"
}

PUT /v1/_settings/{key}

Adds a new custom app setting or overwrites an existing one. To overwrite all app settings, make a PUT request without providing the key parameter, like so:

PUT /v1/_settings
{
    "fb_app_id": "123U3VTNifLPqnZ1W2",
    "fb_secret": "YXBwOnBhcmE11234151667",
    "signin_success": "/dashboard",
    "signin_failure": "/signin?error"
}

This will replace all app-specific settings with the JSON object in that request.

Request

  • body - a JSON object with a single value field: { "value": "setting_value" }, or an object containing all app-specific configuration properties.
  • {key} - a key from the settings map (optional). If {key} is missing, all app settings will be overwritten by the JSON in the body of the request.

Response

Returns an empty response.

  • status codes - 200

GET /v1/_setup/{app_name}

Requires authentication with the root app access/secret keys

Creates a new child app and generates the first pair of API keys. Equivalent to calling Para.newApp() from your Java code.

Request

  • {app_name} - the name of the child app

Parameters

  • sharedTable - Set it to false if the app should have its own table
  • sharedIndex - Set it to false if the app should have its own index
  • creatorid - The id of the User who will be the owner of this app

Response

Returns the access and secret keys for this application which will be used for request signing.

  • status codes - 200, 403

GET /v1/_setup

This resource is publicly accessible without authentication.

Creates the root application and generates the first pair of API keys. Calling this method will enable you to access the REST API.

Request

No parameters.

Response

Returns the access and secret keys for this application which will be used for request signing.

  • status codes - 200

Example response:

{
    "secretKey": "U3VTNifLPqnZ1W2S3pVVuKG4HOVbimMocdDMl8T69BB001AXGZtwZw==",
    "accessKey": "YXBwOnBhcmE=",
    "info": "Save the secret key! It is showed only once!"
}

Subsequent calls to this method return:

{
    "code": 200
    "message": "All set!"
}

POST /v1/_newkeys

This will reset your API secret key by generating a new one. Make sure you save it and use it for signing future requests.

Request

No parameters.

Response

Returns the access and secret keys for this application which will be used for request signing.

  • status codes - 200

Example response:

{
    "secretKey": "U3VTNifLPqnZ1W2S3pVVuKG4HOVbimMocdDMl8T69BB001AXGZtwZw==",
    "accessKey": "YXBwOnBhcmE=",
    "info": "Save the secret key! It is showed only once!"
}

GET /v1/utils/{method}

Utility functions which can be accessed via the REST API include (listed by the method’s short name):

  • newid - calls Utils.getNewId(). You must set para.worker_id to be different on each node.
  • timestamp - calls Utils.timestamp()
  • formatdate - calls Utils.formatDate(format, locale), additional parameters: format (e.g. “dd MM yyyy”), locale.
  • formatmessage - calls Utils.formatMessage(msg, params), additional parameters: message (the message to format)
  • nospaces - calls Utils.noSpaces(str, repl), additional parameters: string (the string to process)
  • nosymbols - calls Utils.stripAndTrim(str), additional parameters: string (the string to process)
  • md2html - calls Utils.markdownToHtml(md), additional parameters: md (a Markdown string)
  • timeago - calls HumanTime.approximately(delta), additional parameters: delta (time difference between now and then)

Example: GET /v1/utils?method=nospaces&string=some string with spaces

Request

  • {method} - the name of the method to call (one of the above)

Response

Returns a JSON object.

  • status codes - 200, 400 (if no method is specified)

Example response - returns the result without envelope:

"result"

POST /v1/_reindex

Rebuilds the entire search index by reading all objects from the data store and reindexing them to a new index. This operation is synchronous - the request will return a response once reindexing is complete.

Example: POST /v1/_reindex

Request parameters

  • destinationIndex - the name of the new index (optional).
  • Use only if you have created the new destination index manually*.

Response

Returns a JSON object.

  • status codes - 200, 404

Example response - returns the result without envelope:

{
   "reindexed": 154,
   "tookMillis": 365
}

GET /v1/_export

Exports a ZIP backup archive of all the data in a Para app. The archive contains multiple JSON files containing the Para objects.

Response

Returns application/zip response - the ZIP backup file.

  • status codes - 200

PUT /v1/_import

Imports a ZIP backup archive into an app, overwriting all existing objects.

Request

Accepts only a application/zip request body which should be a previously exported ZIP backup file.

Parameters

  • filename - the name of the file which is being restored. This will be saved as a record of the executed import job.

Response

Returns an import summary object as JSON.

  • status codes - 200, 400 (if import job failed)

Example response - returns the result without envelope:

{
  "id": "1232047097503551488",
  "timestamp": 1603827503065,
  "type": "paraimport",
  "appid": "test1",
  "count": 4502,
  "name": "myapp_20201026_155306.zip"
}