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:
Let's dive into it step by step:
Step 1 Create Your Admin Identity
Users in OfficeX are merely crypto wallets. Let's create them!
There are three ways to create your admin identity:
Self Custody External ICP wallet (Internet Computer wallet such as Plug Wallet, OISY Wallet)
Convenience cloud route
/generate-crypto-identityOffline 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. 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.
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
};
Step 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, 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.
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.
Step 3 Redeem Factory Giftcard
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".
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.
Step 4 Activate Workspace
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.
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:
Let's dive into it step by step:
Step 1 Create Crypto Identity
This is the same process as shown in Create Admin Identity. For brevity, please look there for code documentation and explanations.
Save the OfficeX UserID to your own database for each reference.
Step 2 Create API Key (optional)
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 signatures. 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 signatures, but for persistence and convenience, we recommend using api keys (the availability of that temp auth flow is why this step is technically optional).
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.
Step 3 Create Auto-Login URLs (optional)
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.
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:
Let's dive into it step by step, looking at the actual real code.
Step 2 Save IDs to your database
Then we save the generated officex organization & user credentials, to your own database solution.
Step 3 Render iFrames on Frontend
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 profiles 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.
Last updated