Skip to main content
Bubl Cloud Documentation
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

2 Develop Back End

Overview of back-end flow

  1. Receive rest-call through controller
  2. Parse request and do logic
  3. Return response
  4. Specific functions (get data, send email, get access token, do payment)
  5. Make an allowed outside connection using proxy

1. Receive rest-call through controller

For any request to your service the controller in the bubl calls your service.

The controller calls your service as command line application.

Command line arguments The executable binary needs to support the following command line arguments

-p <the relative path of the api>
-m <the method, for example GET>
-r <the request body, for example {"key":"value"}
-d <the domain of this bubl>, 0.0.0.0:internalport;<bubldomain> 
-t <token to call data store in bubl>
-pr <proxy url for to connect to external domains>
-e <environment, dev, uat, prd etc>

example request

Example request to service 16db593e-3c0d-4822-a500-9a768d227421

curl --location 'https://<bubl_domain>/service/v1/16db593e-3c0d-4822-a500-9a768d227421/api/contacts/view' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer {{service_token}}' \
--data '{
    "value": "hello"
}'

Will result in the following commond line arguments.

-p /api/contacts/view
-m POST
-r <base64endoded json request body>
-d 0.0.0.0:8080;<bubl_domain>
-t <token>
-pr http://127.0.0.1:9090
-e prd

The base64 encoded request body and the token are further explained below.

2. parse request and do logic

parse command line arguments

The first step you most likely want to do is to parse the command line arguments.

How to to this depends on the programming language you have chosen. It is the same as building a command line application.

var path, method, request, domain, bublDomain, token, proxyUrl, env string

func init() {
   const (
   	pathValue     = "/"
   	pathUsage     = "relative api path"
   	methodValue   = "GET"
   	methodUsage   = "method for api"
   	requestValue  = ""
   	requestUsage  = "api request - base64 encoded json string"
   	domainValue   = ""
   	domainUsage   = "instance domain of bubl"
   	tokenValue    = "testtoken"
   	tokenUsage    = "access token for connect client authentication"
   	proxyUrlValue = ""
   	proxyUrlUsage = "proxy url to access outside urls"
   	envValue      = "dev"
   	envUsage      = "environment"
   )
   flag.StringVar(&path, "path", pathValue, pathUsage)
   flag.StringVar(&path, "p", pathValue, pathUsage+" (shorthand)")
   flag.StringVar(&method, "method", methodValue, methodUsage)
   flag.StringVar(&method, "m", methodValue, methodUsage+" (shorthand)")
   flag.StringVar(&request, "request", requestValue, requestUsage)
   flag.StringVar(&request, "r", requestValue, requestUsage+" (shorthand)")
   flag.StringVar(&domain, "domain", domainValue, domainUsage)
   flag.StringVar(&domain, "d", domainValue, domainUsage+" (shorthand)")
   flag.StringVar(&token, "token", tokenValue, tokenUsage)
   flag.StringVar(&token, "t", tokenValue, tokenUsage+" (shorthand)")
   flag.StringVar(&proxyUrl, "proxy", proxyUrlValue, proxyUrlUsage)
   flag.StringVar(&proxyUrl, "pr", proxyUrlValue, proxyUrlUsage+" (shorthand)")
   flag.StringVar(&env, "env", envValue, envUsage)
   flag.StringVar(&env, "e", envValue, envUsage+" (shorthand)")
   flag.Parse()
}
use clap::{Arg, Command};
use base64::{Engine as _, engine::general_purpose};
use std::str;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Person {
  name: String,
}

#[derive(Serialize, Deserialize)]
struct Response {
  answer: String,
}

fn main() {
   let matches = Command::new("Bubl Cloud Hello World")
       .version("0.1.0")
       .author("Niek Temme <Niek@bubl.cloud>")
       .about("example rust back-end service")
       .arg(Arg::new("path")
                .short('p')
                .help("the relative path of the api"))
       .arg(Arg::new("method")
                .short('m')
                .help("the method, for example GET"))
       .arg(Arg::new("request_body")
                .short('r')
                .help("the request body"))
       .get_matches();

   let default_method = "GET".to_string();
   let default_path = "/".to_string();
   let method: &String = matches.get_one::<String>("method").unwrap_or(&default_method);
   let path: &String = matches.get_one::<String>("path").unwrap_or(&default_path);
   let request_body: &String = matches.get_one::<String>("request_body").unwrap();
  

   // decode the request body
   let decoded = &general_purpose::STANDARD.decode(request_body).unwrap();
   let decoded_slice = &decoded.as_slice();
   let request_body_decoded: &str = str::from_utf8(&decoded_slice).unwrap();
   let request_body_decoded_string = request_body_decoded.to_string();
}

C Content

C++ Content

C# Content

Route input to appropriate function

The second step is to route the input the an appropriate functions.

You can use if/else or case statments in the command line values to route the input. The example code below first routes based request method and then on the relative path.

NOTE ON SPECIAL PATHS!
The /install and /update paths are reserved for special operations. These endpoints are triggered automatically when your service is installed or updated. Typically, you should use /install to create any necessary service tables and /update to modify or migrate existing tables as needed. However, you can also include any additional setup or update logic required for your service during these events.

   // check if path is special
   switch c.Path {
   case "/install":
   	err := s.connect.CreateTable(context.Background(), &connect.ServiceDataTableRequest{
   		TableName: servicetableName,
   	})
   	if err != nil {
   		sendResultWithStatus(http.StatusInternalServerError, Errors{Errs: []string{err.Error()}}, true)
   		return
   	}
   	sendResponse(http.StatusOK, nil)
   	return
   case "/update":
   	sendResponse(http.StatusOK, nil)
   	return
   }
   // decode request
   var decodedRequest string
   if c.Request != "" {
   	dr, err := base64.StdEncoding.DecodeString(c.Request)
   	if err != nil {
   		sendResponse(http.StatusInternalServerError, errResponse(err))
   		return
   	}
   	decodedRequest = string(dr)
   }
   // check method
   switch c.Method {
   case "GET":
   	s.getRequests(c.Path, decodedRequest)
   case "POST":
   	s.postRequests(c.Path, decodedRequest)
   case "PUT":
   	s.putRequests(c.Path, decodedRequest)
   case "DELETE":
   	s.deleteRequests(c.Path, decodedRequest)
   default:
   	sendResponse(http.StatusMethodNotAllowed, nil)
   }

func (s *Service) getRequests(path, req string) {
   switch {
   case strings.Contains(path, "/v1/data/"):
   	key := strings.TrimPrefix(path, "/v1/data/")
   	s.GetData(key)
   case path == "/v1/listdata":
   	s.ListData()
   case path == "/v1/environment":
   	s.GetEnvironment()
   default:
   	sendResponse(http.StatusNotFound, nil)
   }
}

func (s *Service) postRequests(path, req string) {
   switch {
   case path == "/v1/data":
   	s.CreateNewData(req)
   default:
   	sendResponse(http.StatusNotFound, nil)
   }
}

func (s *Service) putRequests(path, req string) {
   switch {
   case strings.Contains(path, "/v1/data/"):
   	key := strings.TrimPrefix(path, "/v1/data/")
   	s.UpdateData(key, req)
   default:
   	sendResponse(http.StatusNotFound, nil)
   }
}

func (s *Service) deleteRequests(path, req string) {
   switch {
   case strings.Contains(path, "/v1/data/"):
   	key := strings.TrimPrefix(path, "/v1/data/")
   	s.RemoveData(key)
   default:
   	sendResponse(http.StatusNotFound, nil)
   }
}
fn main() {
// main function above extended
    match method.as_str() {
        // Match a single value
        "POST" => post_requests(path, &request_body_decoded_string),
        _ => println!("Ain't special"),
        // TODO ^ Try commenting out this catch-all arm
    }
}

fn post_requests(path: &String, request_body: &String) {
    match path.as_str() {
        // Match a single value
        "/hello" => hello_answer(request_body),
        _ => println!("Ain't special"),
        // TODO ^ Try commenting out this catch-all arm
    }
}

fn hello_answer(request_body: &String) {

    let strbody = request_body.as_str();
    let p: Person = serde_json::from_str(&strbody).unwrap();
    let start = "Nice to meet you";
    let resp = start.to_string() + " " + &p.name;
    let response = Response {
        answer: resp
    };
    let j = serde_json::to_string(&response).unwrap();
    let j_bytes = j.as_bytes();
    let encoded: String = general_purpose::STANDARD.encode(j_bytes);
    println!("{}", encoded);
}

C Content

C++ Content

C# Content

Your logic

After you have prased and routed the incomming request you can write your code. This can be any code you want. Ranging from a simple string parsing function to a full machine learning model. What you do and how you want to do it, that is up to you.

3. Return a response

As this service is a command line binary, and is invoved as such, the response is also expected as a stdout.

func sendResponse(httpStatus int, body json.RawMessage) {
   resp, _ := json.Marshal(&Response{
   	ApplicationName:    applicationName,
   	ApplicationVersion: applicationVersion,
   	HTTPStatus:         httpStatus,
   	Body:               body,
   })
   response(string(resp))
}
func response(resp string) {
   response := `{ "response" : ` + resp + `}`
   fmt.Println(base64.StdEncoding.EncodeToString([]byte(response)))
}

Here, body is a json value of the data you want to return.

4. Special functions

Most likely you want to do something with data when the app is running. For this you can ask the controller. There a four special functions. Accessing data, stored in the Bubl, is probably the most imporant.

  1. Accessing data
  2. Request a token
  3. Send an email
  4. Initiate payment

1. Accessing data

Accessing data is handeled through a special endpoint of the controller running inside the Bubl. This endpoint is only accessable from within the Bubl. You always call this endpoint with the token that was sent to you by the controller.

Service data versus common data.

As a service you have access to both

  1. Your own data, specific to your service.
  2. Common data. For example health records than can be accessed by many services.
Service data

For service data you can create your own tables. You usually call this when the app is installed or updated. Your service always has access to all service data of your service.

Common data

Please refer Common Data documentation to understand existing common data page.

When inserting/requesting data to/from a common data model, you can follow this step to create tablename domain_level00_tablename, for example: if you want to request data from profile domain with level 1 and table profiledata, your requested tablename will be profile_100_profiledata

Endpoints

The endpoints that are availale to you are. Use the token provided a flag -t as authorization header when this endpoint is called.

  • /servicedata/data/"+tableName With the methods
  • Post
  • Get
  • Put
  • Delete The example code is provided below.

2. Request a custom token

You can request a custom token from the controller, which can be provided to another party to grant limited, controlled access to specific endpoints or data within your service.

Endpoint

Use the token provided a flag -t as authorization header when this endpoint is called. The available endpoint is:

  • /controller/servicetoken
    Method: PUT

When requesting a custom token, you specify which endpoints and data the token holder is allowed to access. This enables fine-grained access control for external integrations or delegated operations.

type ServiceCustomTokenRequest struct {
   UserDescription               string                      `json:"userDescription"`
   AllowedServiceEndpoints       map[string]string           `json:"allowedServiceEndpoints"`
   AllowedUnstructuredDataCommon map[string]CommondataParams `json:"allowedUnstructuredDataCommon"`
   EndDate                       int64                       `json:"endDate"`
}
type CommondataParams struct {
   Level  string `json:"level" binding:"required"`
   Method string `json:"method" binding:"required"`
}
type ServiceCustomToken struct {
   AccessToken string `json:"accessToken"`
}

ServiceCustomTokenRequest fields explained

  • UserDescription: A string describing the purpose or intended use of the custom token. This is for your own reference or for audit purposes.
  • AllowedServiceEndpoints: A map where the key is the endpoint path (string) and the value is the allowed HTTP method (string, e.g., “GET”, “POST”), can be a comma separated string. This restricts which endpoints the token holder can access and with which methods.
  • AllowedUnstructuredDataCommon: A map where each key is the name of a common data table (string), and each value is a CommondataParams struct. The CommondataParams struct has two fields:
    • Level (string, required): Specifies the access level for the common data table.
    • Method (string, required): Specifies the allowed HTTP method(s) (e.g., “GET”, “POST”) for operations on that table. This field determines which common data tables the token holder can access and what operations they are permitted to perform on each table.
  • EndDate: An integer (int64) representing the expiration time of the token as a Unix timestamp (seconds since epoch). After this time, the token will no longer be valid.

3. Send an email

If you need to send an email to someone, you can do so by requesting controller.

Endpoint

The endpoints that are availale to you are.

  • /email/sendemail With the method
  • Post
    type Email struct {
       ToEmails  []*E   `json:"toEmails"`
       FromEmail *E     `json:"fromEmail"`
       Subject   string `json:"subject"`
       BodyHTML  string `json:"bodyHTML"`
       BodyText  string `json:"bodyTxt"`
    }
    type E struct {
       Email string `json:"email"`
       Name  string `json:"name"`
    }
    
    The example code is provided below.

4. Initiate payment

If you need to initiate a payment you can do so by requesting controller.

Endpoint

The endpoints that are availale to you are.

  • /payments/initpayment POST
  • /payments/status?bublPaymentUuid={bublpaymentid} GET The initpayment gives bublPaymentUuid as response, with some other information, you should store it somewhere to check status later.

type InitPaymentRequest struct {
   Amount      float64 `json:"amount,omitempty"`
   Currency    string  `json:"currency,omitempty"`
   Description string  `json:"description,omitempty"`
   Forward     string  `json:"forward,omitempty"`
   Language    string  `json:"language,omitempty"`
}
type InitPaymentResponse struct {
   BublPaymentUUID string  `json:"bublPaymentUuid"`
   Amount          float64 `json:"amount"`
   Currency        string  `json:"currency"`
   Language        string  `json:"language"`
}
type PaymentStatus struct {
   BublPaymentUUID string `json:"bublPaymentUuid"`
   Status          int    `json:"status"`
   StatusInfo      string `json:"statusInfo"`
}
The example code is provided below.

Example code for calling controller endpoints

Calling the internal controller

Use first part of the “d” flag as server and “t” flag as token to generate a client. For example in the call below call http://0.0.0.0:8080 with the token passed on -t

-p /api/contacts/view
-m POST
-r <base64endoded json request body>
-d 0.0.0.0:8080;615916bubl311087bubl360129.0.0.0.0.1.0.dev.bubl.cloud
-t <token>
-pr http://127.0.0.1:9090
-e dev

Example

  package rest

import (
   "bytes"
   "context"
   "encoding/json"
   "fmt"
   "io"

   "net/http"
)

const authHeader = "Authorization"
const tokenType = "Bearer"

// Client ...
type Client struct {
   client      *http.Client
   server      string
   serverToken string
   readAll     func(io.Reader) ([]byte, error)
}

// NewConnectService ...
func NewConnectService(server string, token string) *Client {
   return &Client{
   	server:      server,
   	serverToken: token,
   	client: &http.Client{
   		Transport: &http.Transport{
   			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
   		},
   	},
   	readAll: io.ReadAll,
   }
}

// CreateTable ...
func (c *Client) CreateTable(ctx context.Context, sdtreq *connect.ServiceDataTableRequest) error {
   return c.doRequest(ctx, http.MethodPut, "/servicedata/table", sdtreq, nil)
}

func (c *Client) DropTable(ctx context.Context, sdtreq *connect.ServiceDataTableRequest) error {
   return c.doRequest(ctx, http.MethodDelete, "/servicedata/table", sdtreq, nil)
}

// AddServiceData adds data to service data or allowed common data
func (c *Client) AddServiceData(ctx context.Context, tableName string, sdreq *connect.ServiceDataRequest) error {
   return c.doRequest(ctx, http.MethodPost, "/servicedata/data/"+tableName, sdreq, nil)
}

// RemoveServiceData deletes data from service data or allowed common data
func (c *Client) RemoveServiceData(ctx context.Context, tableName string, sdreq *connect.ServiceDataRequest) error {
   return c.doRequest(ctx, http.MethodDelete, "/servicedata/data/"+tableName, sdreq, nil)
}

// GetServiceData gets data from service data or allowed common data
func (c *Client) GetServiceData(ctx context.Context, tableName string, sdreq *connect.ServiceDataRequest) (*connect.ServiceData, error) {
   res := &connect.ServiceData{}
   return res, c.doRequest(ctx, http.MethodGet, "/servicedata/data/"+tableName, sdreq, res)
}

// ListServiceData gets list of data from service data or allowed common data
func (c *Client) ListServiceData(ctx context.Context, tableName string, sdreq *connect.ServiceDataRequest) ([]*connect.ServiceData, error) {
   var res []*connect.ServiceData
   return res, c.doRequest(ctx, http.MethodGet, "/servicedata/data/"+tableName, sdreq, &res)
}

// GetCustomToken gets the custom token
func (c *Client) GetCustomToken(ctx context.Context, req *connect.ServiceCustomTokenRequest) (*connect.ServiceCustomToken, error) {
   resp := &connect.ServiceCustomToken{}
   return resp, c.doRequest(ctx, http.MethodPut, "/controller/servicetoken", req, resp)
}

// SendEmail sends an email to user
func (c *Client) SendEmail(ctx context.Context, req *connect.Email) error {
   return c.doRequest(ctx, http.MethodPost, "/email/sendemail", req, nil)
}

// InitPayment initiates a payment
func (c *Client) InitPayment(ctx context.Context, req *connect.InitPaymentRequest) (*connect.InitPaymentResponse, error) {
   resp := &connect.InitPaymentResponse{}
   return resp, c.doRequest(ctx, http.MethodPost, "/payments/initpayment", req, resp)
}

// GetPaymentStatus gives the status of a payment id
func (c *Client) GetPaymentStatus(ctx context.Context, paymentuuid string) (*connect.PaymentStatus, error) {
   resp := &connect.PaymentStatus{}
   return resp, c.doRequest(ctx, http.MethodGet, "/payments/status?bublPaymentUuid="+paymentuuid, nil, resp)
}

func (c *Client) doRequest(ctx context.Context, method string, path string, body, response interface{}) error {
   var bodyReader io.Reader
   if body != nil {
   	reqBody, err := json.Marshal(body)
   	if err != nil {
   		return err
   	}
   	bodyReader = bytes.NewBuffer(reqBody)
   }
   req, err := http.NewRequest(method, c.server+path, bodyReader)
   if err != nil {
   	return err
   }
   req.Header.Add("Content-Type", "application/json")
   req.Header.Add(authHeader, tokenType+" "+c.serverToken)
   resp, err := c.client.Do(req)
   if err != nil {
   	return err
   }
   defer resp.Body.Close()
   switch resp.StatusCode {
   case http.StatusOK, http.StatusCreated:
   case http.StatusNoContent:
   	return connect.ErrNoData
   default:
   	return fmt.Errorf("cannot reach connect service %s", resp.Status)
   }

   if response == nil {
   	return nil
   }

   data, err := c.readAll(resp.Body)
   if err != nil {
   	return err
   }
   if err := json.Unmarshal(data, response); err != nil {
   	return err
   }

   return nil
}
# some code
print("This line will be printed.")

Rust Content

C Content

C++ Content

C# Content

5. Make an allowed outside connection using proxy

If you want to connect to outside your bubl, you can do so using a proxy client, where the allowed domains are monitored, currently only bubl.cloud is allowed but we would soon add request other domains in the developer dashboard, when you make a release of your service. Use “pr” flag as server and “t” flag as token to generate a client. Now you can use this proxy client to reach outside. The certs are already present at the location mentioned when service is installed.

For example in the call below call http://127.0.0.1:9090 with the token passed on -t

-p /api/contacts/view
-m POST
-r <base64endoded json request body>
-d 0.0.0.0:8080;615916bubl311087bubl360129.0.0.0.0.1.0.dev.bubl.cloud
-t <token>
-pr http://127.0.0.1:9090
-e dev

How to create a proxy client

const (
   authHeader      = "Authorization"
   tokenType       = "Bearer"
   ProxyAuthHeader = "Proxy-Authorization"
)
// Client ...
type Client struct {
   client  *http.Client
   readAll func(io.Reader) ([]byte, error)
}

// NewPlatformService ...
func NewPlatformService(proxyurl, token string) (*Client, error) {
   proxyUrl, err := url.Parse(proxyurl)
   if err != nil {
   	return nil, err
   }
   cacerts, err := os.ReadFile("/etc/ssl/cert.pem")
   if err != nil {
   	return nil, err
   }
   pool := x509.NewCertPool()
   if !pool.AppendCertsFromPEM(cacerts) {
   	return nil, err
   }
   return &Client{
   	client: &http.Client{
   		Transport: &http.Transport{
   			Proxy: http.ProxyURL(&url.URL{
   				Scheme: proxyUrl.Scheme,
   				User:   url.UserPassword("", token),
   				Host:   proxyUrl.Host,
   			}),
   			TLSClientConfig: &tls.Config{
   				RootCAs: pool,
   			},
   		},
   	},
   	readAll: io.ReadAll,
   }, nil
}