Advanced Setup

Multi-Stage Deployments for Granular Control

While the 2 min quickstart is sufficient for most integrations, some companies may want more granular control and security around user custody and how those identities are created.

For example, you are a Discord clone that wants to provide anon workspaces for your users. You have 10,000 "communities" with members, each of whom are a "user", and users can belong to many communities. In total you have 1M users.

This Advanced Setup Guide will show how to run a fully custom setup. This method is mandatory if you are running OfficeX servers on a trustless decentralized web3 cloud (ICP canisters).

Create New Organization

The advanced process outline:

1

Create Your Admin Identity

Users in OfficeX are merely crypto wallets, which we can generate offline.

2

Create Factory Giftcard

Imagine OfficeX as a vending machine factory that spawns workspaces, purchased with free giftcards. Create as many free giftcards as you like.

3

Redeem Factory Giftcard

Pay the vending machine with your free giftcard. It will deploy your new workspace inside an encrypted multi-tenant web2 linux server or to the blockchain as a web3 server (ICP canister)

4

Activate Workspace

Your anon workspace is deployed and blank, waiting for you to activate it. This is the final step, welcome home.

Let's dive into it step by step:

chevron-rightStep 1 Create Your Admin Identityhashtag

Users in OfficeX are merely crypto wallets. Let's create them!

There are three ways to create your admin identity:

  1. Self Custody External ICP wallet (Internet Computer wallet such as Plug Walletarrow-up-right, OISY Walletarrow-up-right)

  2. Convenience cloud route /generate-crypto-identity

  3. Offline using your own code (most securely scaleable)

We recommend using Option #2 of /generate-crypto-identity as its the easiest. Understand the tradeoffs below. We also recommend reading the Authentication Guide.

Option #1 - Self Custody External ICP Wallet

Choose this option if you want to use a multi-sig as the superadmin owner of the workspace. This is particularly useful if you are a DAO (decentralized autonomous organization).

1. Download your ICP wallet and copy the principal id of your wallet
2. Append `UserID_` to it, and its now a valid OfficeX UserID

For example:
const principal_id = "b5sy2-4ramt-jojso-cqmlj-rxwek-xe3f6-6tuez-zhrl2-pi65a-dtcd6-tae";
const officex_user_id = `UserID_${principal_id}`

console.log(officex_user_id)
// returns "UserID_b5sy2-4ramt-jojso-cqmlj-rxwek-xe3f6-6tuez-zhrl2-pi65a-dtcd6-tae"

Now you can use this user id as your admin, in the subsequent future steps.

Option #2 - Convenience cloud route /generate-crypto-identity

Choose this option if you want convinence. Our free public cloud will handle creating the user. Beware of unofficial servers that may log your generated crypto identities. While those crypto wallets are not used to hold money, they can sign 30 second auth tokens to interact with REST API on your behalf. Read the Authentication Guide to learn more.

Only trust servers from the domain officex.app or anonwork.space , for example, https://us-east-1.officex.app or https://ap-northeast-1.anonwork.space . We maintain a list of valid domains in the Authentication Guide.

The secret_entropy can be any string, often a concatenation of your database user id plus some secret string. It will lead to the same deterministic user generated every time.

The seed_phrase , if you choose to use this method, must be a valid BIP-39 Mnemonic Seed from this wordlist.arrow-up-right Both methods are valid, we recommend using secret_entropy as its flexibly adaptable to your existing user ids.

import { IRequestGenerateCryptoIdentity } from "officexapp/types"

const payload: IRequestGenerateCryptoIdentity = {
    secret_entropy?: string;
    seed_phrase?: string;
};

const admin: IResponseGenerateCryptoIdentity = await (
    await fetch(`https://officex.otterpad.cc/v1/factory/helpers/generate-crypto-identity`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    })
  ).json();

interface IResponseGenerateCryptoIdentity {
  user_id: UserID; // only the user_id is needed for subsequent steps
  icp_principal: string;
  evm_public_key: string;
  evm_private_key: string;
  origin: {
    secret_entropy?: string;
    seed_phrase?: string;
  };
}

Option #3 - Offline using your own code

Choose this option if you want to maintain a high bar of security over custodial users while using 3rd party servers. It is simply running the same code that powers Option #2 /generate-crypto-identity , except on your own offline servers. You can see proof of this by testing the same secret_entropy string in both.

terminalRun in Codepen

import { wordlist } from "@scure/bip39/wordlists/english";
import { generateMnemonic } from "@scure/bip39";
import { Ed25519KeyIdentity } from "@dfinity/identity";
import { mnemonicToAccount } from "viem/accounts";
import { bytesToHex, sha256, toBytes } from "viem";
import * as bip39 from "bip39";
import { mnemonicToSeedSync } from "@scure/bip39";
import {
  IRequestGenerateCryptoIdentity,
  IDPrefixEnum,
  UserID,
} from "@officexapp/types";

export const generateCryptoIdentity = async (
  args: IRequestGenerateCryptoIdentity
): Promise<{
  user_id: UserID;
  icp_principal: string;
  evm_public_key: string;
  evm_private_key: string;
  origin: {
    secret_entropy?: string;
    seed_phrase?: string;
  };
}> => {
  const { secret_entropy, seed_phrase } = args;

  if (!secret_entropy && !seed_phrase) {
    // create a user completely from scratch
    const seed = generateRandomSeed();
    const wallets = await seed_phrase_to_wallet_addresses(seed);

    const cryptoIdentity = {
      user_id: `${IDPrefixEnum.User}${wallets.icp_principal}`,
      icp_principal: wallets.icp_principal,
      evm_public_key: wallets.evm_public_address,
      evm_private_key: wallets.evm_private_key,
      origin: {
        secret_entropy,
        seed_phrase,
      },
    };

    return cryptoIdentity;
  } else if (seed_phrase) {
    const wallets = await seed_phrase_to_wallet_addresses(seed_phrase);
    const cryptoIdentity = {
      user_id: `${IDPrefixEnum.User}${wallets.icp_principal}`,
      icp_principal: wallets.icp_principal,
      evm_public_key: wallets.evm_public_address,
      evm_private_key: wallets.evm_private_key,
      origin: {
        secret_entropy,
        seed_phrase,
      },
    };
    return cryptoIdentity;
  } else if (secret_entropy) {
    const seed = passwordToSeedPhrase(secret_entropy);
    const wallets = await seed_phrase_to_wallet_addresses(seed);
    const cryptoIdentity = {
      user_id: `${IDPrefixEnum.User}${wallets.icp_principal}`,
      icp_principal: wallets.icp_principal,
      evm_public_key: wallets.evm_public_address,
      evm_private_key: wallets.evm_private_key,
      origin: {
        secret_entropy,
        seed_phrase,
      },
    };
    return cryptoIdentity;
  } else {
    throw new Error("Invalid arguments");
  }
};

// Helper function to generate a random seed phrase
const generateRandomSeed = (): string => {
  // return (generate(12) as string[]).join(" ");
  return generateMnemonic(wordlist, 128);
};

const seed_phrase_to_wallet_addresses = async (seedPhrase: string) => {
  try {
    // For EVM address generation
    const evmAccount = mnemonicToAccount(seedPhrase);
    const evmAddress = evmAccount.address;

    const derivedKey = await deriveEd25519KeyFromSeed(
      mnemonicToSeedSync(seedPhrase || "")
    );
    // Create the identity from the derived key
    // @ts-ignore
    const identity = Ed25519KeyIdentity.fromSecretKey(derivedKey);

    // Get the principal using the identity's getPrincipal method
    const principal = identity.getPrincipal();
    const principalStr = principal.toString();

    return {
      icp_principal: principalStr,
      evm_public_address: evmAddress,
      // @ts-ignore
      evm_private_key: bytesToHex(evmAccount.getHdKey().privateKey),
      seed_phrase: seedPhrase,
    };
  } catch (error) {
    console.error("Failed to generate addresses:", error);
    throw error;
  }
};

const passwordToSeedPhrase = (password: string) => {
  // 1. Generate a deterministic hash (entropy) from the password.
  const passwordBytes = new TextEncoder().encode(password);

  // The sha256 function from viem returns a hex string.
  const entropyHex = sha256(passwordBytes);

  // 2. Convert the hex string to a Uint8Array using viem's toBytes function.
  const entropyBytes = toBytes(entropyHex);

  // 3. Use bip39.entropyToMnemonic to convert the entropy into a mnemonic.
  // The library expects a Buffer, so we need to convert our Uint8Array.
  return bip39.entropyToMnemonic(Buffer.from(entropyBytes), wordlist);
};

// Function to derive Ed25519 key from seed (uses the first 32 bytes of the seed)
const deriveEd25519KeyFromSeed = async (
  seed: Uint8Array
): Promise<Uint8Array> => {
  const hashBuffer = await crypto.subtle.digest("SHA-256", seed);
  return new Uint8Array(hashBuffer).slice(0, 32); // Ed25519 secret key should be 32 bytes
};
chevron-rightStep 2 Create Factory Giftcardhashtag

Imagine OfficeX as a vending machine factory that spawns workspaces, purchased with free giftcards. Create as many free giftcards as you like, on our free public cloud.

Note that if you are running OfficeX on a trustless decentralized web3 cloud (ICP canisters), giftcards are not free as deployment of smart contracts cost crypto for gas. Find a vendor selling giftcards in the Vendor Marketplace.

terminalRun in Codepen

import { IRequestCreateGiftcardSpawnOrg, BundleDefaultDisk, IResponseCreateGiftcardSpawnOrg, GiftcardSpawnOrgID } from "officexapp/types"

const payload: IRequestCreateGiftcardSpawnOrg = {
    usd_revenue_cents?: number;
    note?: string;
    gas_cycles_included?: number;
    external_id?: string;
    bundled_default_disk?: BundleDefaultDisk;
}

const admin: IResponseCreateGiftcardSpawnOrg = await (
    await fetch(`https://officex.otterpad.cc/v1/factory/giftcards/spawnorg/create`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    })
  ).json();

interface IResponseCreateGiftcardSpawnOrg {
  id: GiftcardSpawnOrgID; // string
  usd_revenue_cents: number;
  note: string;
  gas_cycles_included: number;
  timestamp_ms: number;
  external_id?: string;
  redeemed: boolean;
  bundled_default_disk?: BundleDefaultDisk;
}

Great! All you need is to keep the GiftcardSpawnOrgID to use in subsequent steps.

chevron-rightStep 3 Redeem Factory Giftcardhashtag

Now that you have a GiftcardSpawnOrgID and admin UserID , we can redeem the factory giftcard and receive a fresh new workspace, deployed from the "vending machine".

terminalRun in Codepen

import { IRequestRedeemGiftcardSpawnOrg, IResponseRedeemGiftcardSpawnOrg, GiftcardSpawnOrgID, UserID, DriveID } from "officexapp/types"

const payload: IRequestRedeemGiftcardSpawnOrg = {
    giftcard_id: GiftcardSpawnOrgID;
    owner_user_id: UserID;
    owner_name?: string;
    organization_name?: string;
    external_id?: string;
    email?: string;
}

const admin: IResponseRedeemGiftcardSpawnOrg = await (
    await fetch(`https://officex.otterpad.cc/v1/factory/giftcards/spawnorg/redeem`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    })
  ).json();

interface IResponseRedeemGiftcardSpawnOrg {
    owner_id: UserID;
    drive_id: DriveID;
    host: string;
    redeem_code: string; // important: do not expose this secret
    external_id?: string;
}

Hold on to that returned redeem_code as we need it for the final step (activating our workspace). We also need the returned host and DriveID (aka organization id).

Be careful not to lose it as anyone can use it to activate the org and login as admin with default api keys. Note that this is by design, as sometimes the superadmin owner is a infrequent use multi-sig, which is why we decouple the workspace activation. The owner superadmin wallet is able to deactivate lost api keys via signature auth. Learn more in the Authentication Guide.

chevron-rightStep 4 Activate Workspacehashtag

Finally, we can use the redeem_code to activate our new workspace. This just creates an admin api key, deploys any default storage disks, and provides a convenient auto-login url for browsers.

Note that the host and DriveID strings are also needed to query the right backend server. These were returned to you in the previous step Redeem Factory Giftcard.

terminalRun in Codepen

We are finished! Now you can use the auto_login_url to open OfficeX on web browser and auto login as owner superadmin. You can also use the api_key to interact with the REST API on behalf of owner superadmin. Be sure to save these variables to your own database for easy future reference.

For sanity check, we can query the organizations /whoami endpoint with api_key to verify its working.

Congratulations on creating your new OfficeX Anonymous Workspace!

Next Steps? Check out the REST API reference to see how to create contacts, join groups, create permits & more!

Create New User

In our example, users can belong to many organizations. While the previous script created new organizations along with their admin user, not every user will be an admin. So we must run another process for those users.

The advanced process outline:

1

Create Crypto Identity

Users in OfficeX are merely crypto wallets, which we can generate offline.

2

Create API Key (optional)

Create API Keys per org + user combo. This can be done in bulk or on-the-fly.

3

Create Auto-Login URL (optional)

Get a simple auto-login url you can give users to visit in browser. Also can be done in bulk or on-the-fly. Not necessary if you are primarily using iframes UX within your own app.

Let's dive into it step by step:

chevron-rightStep 1 Create Crypto Identityhashtag

This is the same process as shown in Create Admin Identity.arrow-up-right For brevity, please look there for code documentation and explanations.

Save the OfficeX UserID to your own database for each reference.

chevron-rightStep 2 Create API Key (optional)hashtag

Every unique combo of organization + user has its own "user api key for that organization". It is a many to many relationship. It is up to you to decide how to want to model that relationship in your own database. Likely you already have a join table, such as in our example "Discord Clone" where users can belong to many communities.

Here is how we can create an API Key for a user in an organization. You will need to either use the admin api key to do this, or alternatively allow contacts to generate their own api keys using temporary auth signaturesarrow-up-right. We recommend just using the organization admin api key.

First we must add the user to the organization as a contact. Only contacts can have API Keys. Users can still interact with orgs without api keys simply by using temporary auth signaturesarrow-up-right, but for persistence and convenience, we recommend using api keys (the availability of that temp auth flow is why this step is technically optional).

terminalRun in Codepen

Step 1 - Create Contact

Step 2 - Create API Key

Great! Now you now have an api key representing your user in that organization. Save it to your database for easy future reference.

If you are using iframes to embed OfficeX into your app, this api key is all you need. If you are not using iframes and instead want to let users go to OfficeX directly in their web browser, proceed to the final Step 3 to create auto-login urls for them.

chevron-rightStep 3 Create Auto-Login URLs (optional)hashtag

Auto-Login URLs are convenient if you want to let your users visit OfficeX directly in their web browser. This also saves you work if you don't want to use iframes.

The below code uses the helper route /generate-auto-login-link but its merely a string manipulation wrapper and thus can also be done offline yourself by copying the same backend code seen on Github.arrow-up-right

terminalRun in Codepen

That is all! Now your users can sign in directly to OfficeX on their web browser, into your designated organization and profile.

Bulk Provision Workspaces for 1M users

In our example, we are "Discord Clone" with 10k communities and 1 million users. What is the best way to bulk provision workspaces for them?

If you are using our free public cloud, its very easy just run a for loop script. If you are self hosting, the process is the same but you should read our docs on Self Hosting and Performance Benchmarks.

Here is what the bulk provisioning script looks like in pseudo-code (ie. the code is not real, just an explainer. scroll down to see the actual real code)

For each in loop:

1

Run the Quickstart

Create a workspace for each community, including admin

2

Save IDs to your database

Match org to org, user to user, and possibly api keys join table

3

Render iFrames on Frontend

Using either api keys or deterministic

Let's dive into it step by step, looking at the actual real code.

chevron-rightStep 1 Run the Quickstarthashtag

First we create the organization and decide an admin

chevron-rightStep 2 Save IDs to your databasehashtag

Then we save the generated officex organization & user credentials, to your own database solution.

chevron-rightStep 3 Render iFrames on Frontendhashtag

For the purpose of this demo, we will choose to send api keys to frontend iframe for every attempt of a user navigating to workspace. Simply provide up-to-date IFrameInjectedConfig to client devices.

If your users are often hopping between multiple organizations including entering new ones, we recommend using deterministic iframe profilesarrow-up-right with cloud organizations, as that lets you avoid a lot of headache managing api keys per user per org. The ephemeral offline profiles can use their crypto identities to generate temp auth signatures as a form of universal auth for all organizations. Compare this with other common iframe auth flows.arrow-up-right

Last updated