Custom Apps

A guide to setting up custom web applications that interact with the ThreeKit Platform

Introduction

Custom web applications play a crucial role in enhancing the functionality and efficiency of ThreeKit projects. By integrating custom web apps, developers can automate various processes and create intuitive client-facing user interfaces. These web applications streamline the management of the ThreeKit project's backend, offering significant benefits such as:

  • Enhanced Automation: Custom web apps can automate repetitive tasks, reducing manual intervention and minimizing errors, such as using spreadsheets to import Product Data into the Catalog.
  • Improved UX: Client-facing interfaces allow users to interact seamlessly with the ThreeKit project, making it easier to manage configurations and visualize outcomes.
    An example of this could be a custom app for approvals and review, where the client could preview the current set of items that are ready for review, leave comments, and approve or reject the entries.
  • Scalability: Web apps provide a scalable solution to manage growing project requirements, ensuring that the system can handle increasing loads and complexities.
  • Customization: Developers can tailor web applications to meet specific project needs, offering flexibility and adaptability.

Requirements

Adding custom web apps to a project is pretty straight forward once we have a decent understanding of general web app development. There are plenty of YouTube tutorials and documents out there that explain the web app development process, and there are very few things specific to ThreeKit.

The main goal of a web application is to read/write/update data inside a given ThreeKit org. This is performed through the use of HTTP requests using the ThreeKit REST API. A backend server is required in order to make requests to the API endpoints that require any method other than GET.

Here are a list of the basic requirements for integrating your web application with the ThreeKit project:

Deployment

The main challenge to overcome is the decision of how and where to host the application.

You could either host this application yourself, or you have the option to have it hosted by ThreeKit. On every project we provide the client and implementation team with a GitHub repo where they may upload their code for deployment. The deployment then happens automatically through GitHub Actions on every push to the main branch.

We offer two types of deployments, depending on the intended use of the web app.

  • For front-end only apps that are meant to be end-client facing (such as an app that is used to embed the player on the live production site) we set up a deployment through Cloudflare.
  • For Backend apps that are meant only for backend project automation and maintenance we set up a deployment through Porter.

You can contact the Support Team at [email protected] for additional information on this topic, if you have special requests in terms of server resources.

Multiple Apps

We recommend that you use the same repo for all your backend apps, separating them by folders. This would greatly simplify your implementation, as you can more easily share components and functionality across all the apps, including the authentication data.

Authentication

In order to make requests to the REST API endpoints, the web app needs to authenticate with the ThreeKit platform on every request. There are three main options for this authentication:

1 - Public Tokens

You can authenticate GET requests from the client-side using a public token created within your ThreeKit project's Org. The token ID can be used as the bearer_token in the request.

Public tokens are automatically associated with the org in which they were created, so then can be used on their own for authentication without the need for an orgId to be specified. You can create a public token as shown here.

Here is an example of how to perform a fetch request in TypeScript with a public token from the client-side, on the Get Assets API endpoint.

async function getAssets() {
  const url = new URL(apiURL + `/assets`);
  url.searchParams.set(
    "bearer_token",
    "YOUR_PUBLIC_TOKEN"
  );
  
  try {
    const response = await fetch(url.toString(), {
      method: "GET",
      headers: {
        accept: "application/json",
      },
    });
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error(error);
  }
}

2 - Private Tokens

You can authenticate any requests from the server-side using a private token created within your ThreeKit project's Org. The token ID can be used as the bearer_token in the request.

Private tokens are automatically associated with the org in which they were created, so then can be used on their own for authentication without the need for an orgId to be specified. You can create a private token as shown here.

Here is an example of how to perform a fetch request in TypeScript with a private token from the server side, on the Get Assets API endpoint.

async function getAssets() {
  const url = new URL(apiURL + `/assets`);
  
  try {
    const response = await fetch(url.toString(), {
      method: "GET",      
      headers: {
        accept: "application/json",
        authorization: "Bearer YOUR_PRIVATE_TOKEN_ID",
      },
    });
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error(error);
  }
}

3 - Cookie Tokens

Cookie tokens are a special token provided by the ThreeKit platform automatically when your web application is embedded into an org using the method described here.

When embedded into the org this way, the web application will receive a set of URL parameters from ThreeKit, which include the following:

  • appId - a unique Id assigned to this app embed
  • orgId - the orgId of the current org where the app was embedded and accessed from
  • token - the Cookie token provided to the app for the current session
  • hostname - the environment hostname, such as preview.threekit.com
  • branch - the current branch selected in the platform UI when the app is started.

The Cookie token can then be passed to a REST API request using the following method:

async function getAssets() {
  const url = new URL(apiURL + `/assets`);
  url.searchParams.set("orgId", "YOUR_ORG_ID");
  
  try {
    const response = await fetch(url.toString(), {
      method: "GET",      
      headers: {
        accept: "application/json",
        Cookie: "YOUR_COOKIE_TOKEN_ID",
      },
    });
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error(error);
  }
}

🚧

Warning!

When using the Cookie token, the orgId also needs to be passed to the REST API request, as the Cookie does not include information about the org, unlike the public or private tokens.

Tech Stack

If you plan to deploy the applications yourself with your own hosting solution, then you could use any framework and server solution you wish. There are no special requirements from the ThreeKit side. For example you could use a NodeJS setup with plain React in combination with an Express or Koa server for the backend.

NextJS & TypeScript

If the deployment of the app is done through ThreeKit, then we require that you make use of NextJS with TypeScript. This will allow our team to provide help and support for your applications.

NextJS is an easy to use React-based framework that offers both client and server-side functionality. This greatly simplifies the backend server setup, especially when using their server actions. The NextJS installation also allows for an easy automatic setup and install of TypeScript in combination with the NextJS modules.

The use of TypeScript will help reduce the amount of runtime errors related to mismatched data types. This could become especially problematic with all the various data that would need to be passed and received from the ThreeKit API requests.

Database Support

ThreeKit also offers a PostgreSQL database on request, for web applications that may need the additional benefits a database. This will only be available with applications deployed by ThreeKit. Please reach out to [email protected] for requests and questions in this regard.

Setup

The following workflow example is based on the ThreeKit recommended setup using NodeJS with NextJS and TypeScript.

Installation

  1. First and foremost, ensure you have NodeJS installed on your machine.
  2. Next you will need to clone locally to your machine the empty GitHub repo provided to you by ThreeKit, or have a new project folder linked to the repo.
  3. Install NextJS in the app folder using the npx create-next-app@latest command, enabling at least the TypeScript option and use src folder option during the wizard install process.
  4. Optional - Install the ThreeKit rest-api sdk package if you would like to make use of our TypeScript SDK library for access to the REST API, instead of performing your own fetch or axios commands. Please note that the functionality of the sdk library is NOT YET fully matched to all the options available with the REST API.

Example

The following example shows a basic setup with the above install.

The app simply displays a list of names for the 30 most recently modified Catalog Items found in the org corresponding to the provided private token. It makes use of the Get Assets API endpoint to fetch the list, returning up to a maximum of 30 entries by default.

With Private Token

Please take note of the two tabs in the following code block.

"use client";

import { useEffect, useState } from "react";
import { getItems } from "@/actions/ServerActions";

type Item = {
  id: string;
  name: string;
};

export default function Home() {
  const [items, setItems] = useState<Item[]>([]);

  useEffect(() => {
    getItems()
      .then(setItems)
      .catch((err) => console.log("Error fetching data"));
  }, []);

  return (
    <>
      <div
        style={{ fontWeight: "bold", fontSize: "1.5em", marginBottom: "15px" }}
      >
        List of Most Recent 30 Items
      </div>
      <ul>
        {items.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </>
  );
}
"use server";

export async function getItems() {
  const TOKEN = "YOUR_PRIVATE_TOKEN_ID";
  const url = new URL(`https://preview.threekit.com/api/assets`);
  url.searchParams.set("type", "item");

  try {
    const response = await fetch(url.toString(), {
      method: "GET",
      headers: {
        accept: "application/json",
        authorization: `Bearer ${TOKEN}`,
      },
      cache: "no-store",
    });
    const data = await response.json();
    return data.assets;
  } catch (error) {
    console.error(error);
    return { error };
  }
}

With Cookie Token

Note how in this example we receive the URL parameters from the platform through the searchParams provided by NextJS. In this scenario we are also verifying if these parameters are provided, to ensure that the app was embedded inside an org using the instructions shown here.

The server action to get the Items will then also call the API from the relative hostname, and also apply it to the currently selected branch. This can be done regardless of whether you are using Cookie tokens or private tokens.

Please take note of the two tabs in the following code block.

"use client";

import { useEffect, useState } from "react";
import { getItems } from "@/actions/ServerActions";

type Item = {
  id: string;
  name: string;
};

export default function Home({
  searchParams,
}: {
  searchParams: {
    hostname: string;
    branch: string;
    orgId: string;
    token: string;
  };
}) {
  const { hostname, branch, orgId, token } = searchParams;
  const [items, setItems] = useState<Item[]>([]);

  useEffect(() => {
    if (!hostname || !branch || !orgId || !token) {
      return;
    }
    getItems({ hostname, branch, orgId, token })
      .then(setItems)
      .catch((err) => console.log("Error fetching data"));
  }, [branch, hostname, orgId, token]);

  if (!hostname || !branch || !orgId || !token) {
    return (
      <>
        <div
          style={{
            fontWeight: "bold",
            fontSize: "1.5em",
            marginBottom: "15px",
          }}
        >
          List of Most Recent 30 Items
        </div>
        <span>App not embedded inside a ThreeKit org</span>
      </>
    );
  }

  return (
    <>
      <div
        style={{ fontWeight: "bold", fontSize: "1.5em", marginBottom: "15px" }}
      >
        List of Most Recent 30 Items
      </div>
      <ul>
        {items.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </>
  );
}

"use server";
export async function getItems({
  hostname,
  branch,
  orgId,
  token,
}: {
  hostname: string;
  branch: string;
  orgId: string;
  token: string;
}) {
  const url = new URL("https://" + hostname + "/api/assets");
  url.searchParams.set("orgId", orgId);
  url.searchParams.set("branch", branch);
  url.searchParams.set("type", "item");

  try {
    const response = await fetch(url.toString(), {
      method: "GET",
      headers: {
        accept: "application/json",
        Cookie: token,
      },
      cache: "no-store",
    });
    const data = await response.json();
    return data.assets;
  } catch (error) {
    console.error(error);
    return { error };
  }
}

With rest-api-sdk instead of fetch

In this scenario we make use of the ThreeKit rest-api-sdk to make our HTTP requests, instead of using fetch, for the Cookie token example shown above. This avoids having to remember the URLs, methods and parameters for the API endpoints.

For this purpose we import the ThreekitClient into our ServerActions.tsx file, and define a client using the authentication credentials provided. In addition to that, we can also import the Asset type into our client page, to type the return accordingly.

"use client";

import { useEffect, useState } from "react";
import { Asset } from "@threekit/rest-api";
import { getItems } from "@/actions/ServerActions";

export default function Home({
  searchParams,
}: {
  searchParams: {
    hostname: string;
    branch: string;
    orgId: string;
    token: string;
  };
}) {
  const { hostname, branch, orgId, token } = searchParams;
  const [items, setItems] = useState<Asset[]>([]);

  useEffect(() => {
    if (!hostname || !branch || !orgId || !token) {
      return;
    }
    getItems({ hostname, branch, orgId, token })
      .then((response) => {
        if (response.error) {
          console.log("Error fetching data");
          return;
        } else if (!response.data) {
          console.log("No data received");
          return;
        }
        setItems(response.data);
      })
      .catch((err) => console.log("Error fetching data"));
  }, [branch, hostname, orgId, token]);

  if (!hostname || !branch || !orgId || !token) {
    return (
      <>
        <div
          style={{
            fontWeight: "bold",
            fontSize: "1.5em",
            marginBottom: "15px",
          }}
        >
          List of Most Recent 30 Items
        </div>
        <span>App not embedded inside a ThreeKit org</span>
      </>
    );
  }

  return (
    <>
      <div
        style={{ fontWeight: "bold", fontSize: "1.5em", marginBottom: "15px" }}
      >
        List of Most Recent 30 Items
      </div>
      <ul>
        {items.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </>
  );
}

"use server";
import { ThreekitClient } from "@threekit/rest-api";

export async function getItems({
  hostname,
  branch,
  orgId,
  token,
}: {
  hostname: string;
  branch: string;
  orgId: string;
  token: string;
}) {
  const client = new ThreekitClient({
    orgId,
    host: hostname,
    cookie: token,
    branch,
  });

  try {
    const response = await client.assets.get({ type: "item" });
    if (response.status !== 200) {
      return { error: response.statusText };
    }
    return { data: response.data.assets };
  } catch (error) {
    console.error(error);
    return { error };
  }
}

Local Development

You can certainly develop and test your app locally prior to merging the code into your GitHUb repo. In order to do this, you will need the following:

  • Use private tokens, which are therefore not dependent on the URL domain.
  • If you need to test REST API requests from the client-side, then your public token for local testing needs to include the localhost domain. This is a security risk, so keep this public token private, only for your local development use.
  • If you plan to install the app into your org, then you can use your full localhost URL for the app install, such as http://localhost:3000, including any subdirectories for multiple apps from the same domain, such as: http://localhost:3000/approvals

Deployment

When it comes time to deploy your app, you will first need to inform [email protected] that you've merged in your code. The support team will inspect your package.json file for the listed commands to run a build and start, and set up the deploy actions in the repo.

Once that is done, from that point on simply perform a Push and Merge request to the GitHub repo, and a new deploy will take place automatically.

🚧

Warning!

Please ensure you test a build of your app locally first prior to deploy.

With NextJS you can run the npm run build command to test for build errors.

Then you can test it with npm run start.