Agent index (llms.txt) — fetch this first, then the page it names.
TypeScript SDK
@debros/orama (version 0.3.0, MIT) is the programmatic surface of an Orama namespace. Everything a deployed application does (query its database, publish to a topic, cache a value, store a file, invoke a function) goes through one client against one gateway URL.
The SDK reaches a deliberate subset of the gateway. Deploying an application, minting a key and managing nodes are the CLI's job. Every route and its owner is on Gateway HTTP API. There is also a Go client.
Install
npm install @debros/orama
ESM and CommonJS, in Node and in a browser bundle. Node 20.19 or later. Entry points:
| Import | What |
|---|---|
@debros/orama | The client and everything below |
@debros/orama/chain | Read the chain, build and sign transactions. Optional peers: @bufbuild/protobuf, @noble/curves, @noble/hashes, @scure/base (all 2.x) |
@debros/orama/relay | RelayedFetch for relayed downloads. Node.js only |
The vault client is a separate package, @debros/orama-vault; see Secrets.
The client
import { createClient } from "@debros/orama";
const client = createClient({
baseURL: "https://ns-myapp.cluster.example.com",
apiKey: process.env.ORAMA_API_KEY,
});baseURL is the namespace gateway (ns-<name>.<base-domain>) for data-plane calls, or the cluster gateway (https://<base-domain>) for the lobby and namespace creation.
| Member | What it reaches |
|---|---|
client.auth | Credentials, wallet login, devices, session renewal |
client.db | The namespace's RQLite database |
client.pubsub | Topics: HTTP to publish, WebSocket to subscribe |
client.cache | The Olric distributed cache |
client.storage | IPFS upload, pin, retrieve, fetch capabilities |
client.functions | Serverless function invocation |
client.network | Gateway health, cluster status, peers, the anonymity proxy |
ClientConfig
| Option | Default | Meaning |
|---|---|---|
baseURL | required | Gateway URL |
apiKey | orama_<type>_<payload>_<checksum>. Exchanged for a token before the first request; the key itself never goes on the wire again | |
jwt | An existing session token | |
timeout | 60000 | Per-request timeout, ms |
maxRetries | 3 | Attempts for a retryable status (408, 429, 5xx) |
retryDelayMs | 1000 | Base backoff, multiplied by the attempt and jittered |
debug | false | Print diagnostics. Off, the SDK writes nothing to the console |
storage | memory | Where tokens persist. LocalStorageAdapter in a browser |
wsConfig | WebSocket options, including reconnect | |
functionsConfig | namespace, and an optional separate gatewayURL | |
fetch | platform | Replace the fetch used for every request |
onNetworkError | Called when a request does not reach the gateway; the place to fail over |
Where a key belongs
An API key carries grants, and a key in a browser bundle is a key in the hands of everyone who loads the page.
- +Server side only: an
adminkey. It reaches the control plane: deploys, secrets, key management, raw RQLite. It belongs in CI and on a developer's machine. - +Safe in a client bundle: an
app-runtimekey. Data-plane grants only, and the gateway additionally requires a logged-in user's wallet session for storage, WebRTC and proxy (a deployed app's own workload token also qualifies on storage and WebRTC), so an extracted runtime key on its own cannot act as anybody.
orama namespace keys create --scope app-runtime --label web orama namespace keys create --scope invoke,pubsub,cache --label worker orama namespace keys create --scope admin --label ci
The calls below need these grants. client.db reaches the control plane, so it needs an admin key or a signed-in wallet holding the developer role or above. client.pubsub needs pubsub and client.cache needs cache, which the app-runtime profile leaves out, so name them in --scope. client.storage needs storage plus a signed-in user's session. client.functions needs invoke.
Authentication
Every request carries Authorization: Bearer <token> and nothing else.
API key
const client = createClient({ baseURL, apiKey: process.env.ORAMA_API_KEY });The key is exchanged once, before the first request, for a 15-minute token carrying the key's grants. The SDK exchanges again before it expires. The key stays in the process it was given to: not in an access log, a proxy trace, a devtools tab or a WebSocket URL. getToken() therefore returns a token rather than the key.
A deployed app
import { createWorkloadClient } from "@debros/orama";
const client = await createWorkloadClient();It reads ORAMA_GATEWAY_URL and ORAMA_TOKEN_FILE from the environment the platform set up and renews the token before it expires. What it may reach is whatever orama app grants set <app> … gave it: nothing, until you do.
Wallet login
A user proves ownership of an address by signing a Sign-In with Ethereum message (EIP-4361), or the Solana counterpart for chain_type: "SOL".
const challenge = await client.auth.challenge({
wallet: "0xYourWallet",
namespace: "acme",
chain_type: "ETH", // or "SOL"
});
// Sign the message verbatim. It is what the wallet shows the user.
const signature = await wallet.signMessage(challenge.message);
const session = await client.auth.verify({ message: challenge.message, signature });The message carries the gateway's domain, the namespace, the nonce and a five-minute expiry; verify takes the message, not the fields beside it, so the wallet and namespace it acts on are read out of the bytes the user approved. verify stores the access token, the refresh token and the namespace. Challenges are single use. client.auth.getApiKey({ message, signature }) mints a long-lived key the same way from its own fresh challenge.
Sessions renew themselves
When the gateway answers 401 and a refresh token is stored, the SDK renews the session and replays the request once. Concurrent requests that expire together share one renewal.
await client.auth.logout(); // tell the gateway, clear everything
await client.auth.logout({ keepApiKey: true }); // the user signs out; the app key stays
await client.auth.logout({ server: false }); // clear local state onlyclient.auth.whoami() answers { authenticated: false } for a rejected credential and throws for anything else, so a gateway that is down is not reported as a signed-out user.
Device-bound sessions
See Devices for the model. SDK calls:
client.auth.setDeviceSigner({
publicJwk, // {kty:"EC",crv:"P-256",x,y} or {kty:"OKP",crv:"Ed25519",x}
sign: async (message) => base64url(await platformSign(message)),
});
const challenge = await client.auth.challenge({ wallet, namespace: "acme", device_id: deviceIdOf(publicJwk) });
const result = await client.auth.verify({
message: challenge.message,
signature: await wallet.signMessage(challenge.message),
device_key: publicJwk,
device_signature: base64url(await platformSign(challenge.message)),
device_label: "Alice's phone",
});deviceIdOf(publicJwk) is the RFC 7638 thumbprint. A device-bound verify returns no API key; later refreshes sign a proof with the device key automatically, and without a signer refresh() throws DEVICE_PROOF_REQUIRED. When the namespace requires an existing device's approval, verify returns {status: "pending_approval", user_code, device_code} and stores nothing: show the user code, call client.auth.approveDeviceLink(code) on a device already signed in, then client.auth.claimDeviceLink(device_code, namespace) on the new one. startDeviceLink({namespace}) starts a link with no wallet. listDevices() and revokeDevice(id) manage devices. deviceProofMessage is exported for a client that signs proofs itself.
Persisting tokens in a browser
import { createClient, LocalStorageAdapter } from "@debros/orama";
const client = createClient({ baseURL, apiKey, storage: new LocalStorageAdapter() });client.auth methods
setApiKey, setJwt, getToken, renew, refresh, whoami, logout, logoutUser, clear, challenge, verify, getApiKey, setDeviceSigner, listDevices, revokeDevice, approveDeviceLink, startDeviceLink, claimDeviceLink.
Database
const rows = await client.db.query("SELECT * FROM messages WHERE room = ?", ["general"]);
await client.db.exec("INSERT INTO messages (room, body) VALUES (?, ?)", ["general", "hi"]);
const recent = await client.db.find("messages", { room: "general" }, { limit: 20 });
const one = await client.db.findOne("messages", { id: 42 }); // null when absent
const page = await client.db.createQueryBuilder("messages")
.where("room = ?", ["general"]).orderBy("created_at DESC").limit(20).getMany();
const messages = client.db.repository<Message>("messages");
await messages.save({ room: "general", body: "hi" });A row that is not there is null from both findOne methods, never an exception. Table, primary-key and column names are validated as plain SQL identifiers ([A-Za-z_][A-Za-z0-9_]*); anything else is rejected with INVALID_IDENTIFIER. Values are parameterised.
Methods: exec, query, find, findOne, createQueryBuilder, repository, transaction, createTable, dropTable, getSchema. See Databases.
Pub/sub
await client.pubsub.publish("room:general", "hello");
await client.pubsub.publish("room:general", new Uint8Array([1, 2, 3]));
const subscription = await client.pubsub.subscribe("room:general", {
onMessage: (msg) => {
msg.data; // payload decoded as UTF-8
msg.bytes; // payload exactly as published
msg.timestamp; // server-assigned
},
onError: (error) => report(error),
onClose: () => teardown(),
});
subscription.close();data is lossy for a payload that is not text; read bytes for anything binary. A dropped connection is re-established with exponential backoff (ten attempts by default) and the subscription resumes; onClose fires only when it is finished for good. Presence is opt-in:
await client.pubsub.subscribe("room:general", {
presence: { enabled: true, memberId: userId, onJoin: show, onLeave: hide },
});onLeave fires for a close, a dropped connection, or a client that stops answering the gateway's pings (dropped after 75 seconds of silence). Methods: publish, topics, getPresence, subscribe; a subscription has onMessage, onError, onClose, getPresence, hasPresence, isConnected, close. See Pub/sub.
Cache
await client.cache.put("sessions", userId, { seen: Date.now() }, "1h");
const hit = await client.cache.get("sessions", userId); // null on a miss
await client.cache.delete("sessions", userId);Methods: health, get, put, delete, multiGet, scan. Limits and TTLs are on Cache.
Storage
const { cid } = await client.storage.upload(file, file.name);
const status = await client.storage.status(cid);
const stream = await client.storage.get(cid); // ReadableStream
const response = await client.storage.getBinary(cid); // Response, with headers
await client.storage.unpin(cid);A read that closely follows a write can legitimately 404 while the pin propagates, so reads retry a 404 for about eighteen seconds, but only while the gateway marks it retryable (a CID uploaded or pinned in the last two minutes). Methods: upload, pin, status, get, getBinary, unpin, mintFetchCaps, revokeFetchCap, directTransport, fetchWith. Error codes: Storage. Downloading without the storage node learning your address: Relayed fetch below and the privacy page.
Functions
const client = createClient({ baseURL, apiKey, functionsConfig: { namespace: "myapp" } });
const result = await client.functions.invoke("resize", { cid, width: 400 });functionsConfig.gatewayURL sends invocations to a different origin with the same credentials. Writing and deploying functions: Functions.
Relayed fetch
A relayed fetch downloads a CID without the storage node learning the caller's address. The owner mints capabilities; the downloader presents one through a relay. The model and its limits are on Relayed fetch; this section is the SDK's side of it.
// Owner (device-bound session): mint one token per fetch, 3600..604800 s lifetime.
const { caps } = await client.storage.mintFetchCaps(cid, { count: 3, ttlSeconds: 86400 });
// Each capability carries `revokeKey`, the proof that you issued its id; keep it with the owner.
await client.storage.revokeFetchCap(caps[0].id, caps[0].revokeKey);
// Downloader, Node.js only:
import { RelayedFetch } from "@debros/orama/relay";
const transport = new RelayedFetch({
relays: ["https://node-1.relays.example", "https://node-2.relays.example"],
namespaceHost: "ns-myapp.cluster.example.com", // a plain hostname (letters, digits, hyphens); anything else throws
jitterMs: 2000, // optional: random 0..2 s before the request is sent
circuit: "fresh", // "session" reuses one Tor circuit across a batch
});
const bytes = await client.storage.fetchWith(transport, cid, caps[1], { signal });
// The non-private baseline: the client's own credential, no relay.
const direct = await client.storage.fetchWith(client.storage.directTransport(), cid, caps[2]);For each fetch the SDK picks one relay uniformly at random (never the namespace
host), opens GET /v1/proxy/relay on it with no credential, runs TLS to the
namespace host inside that WebSocket's byte stream, and sends
GET /v1/storage/relayed/<cid> with X-Orama-Fetch-Cap. The relay carries
ciphertext.
What it hides, and what it does not:
| Party | Sees | Does not see |
|---|---|---|
| Relay | your IP address; the namespace host; the size and timing of the stream | the CID, the capability, the content |
| Storage node | the CID and the capability; a Tor exit address | your IP address |
| Anyone watching both ends | timing and size can correlate a fetch (jitterMs blunts this, it does not remove it) |
A relay run by the same operator as the storage node, or in the same cluster, is not a privacy boundary: together they hold both halves. Choose relays from a different operator than the namespace.
- +Node.js only.
RelayedFetchneedsnode:tlsandws, so it lives in the@debros/orama/relayentry and the core import stays bundleable for React Native and browsers. Those apps need a native helper that runs TLS to the namespace host over the relay stream. - +No direct fallback. A relay or tunnel failure throws and is never retried
as a direct request, which would send your address to the storage node.
Falling back is the application's decision, made by calling
fetchWithwith another transport. - +A response must say how long it is. A
200with neitherContent-Lengthnor chunked encoding is refused (RELAY_PROTOCOL_ERROR): a body that ends when the connection does cannot be told from one a reset cut short. - +Pin propagation.
fetchWithre-asks on a retryable 404 exactly asgetdoes, through the same transport, choosing a relay afresh each time. - +A fetch buffers the body (default limit 64 MiB, the relay's per-stream cap;
deadline 5 minutes:
maxBodyBytes,timeoutMs). Passcato trust a private CA for the namespace host.
Errors:
| Error | code | Meaning |
|---|---|---|
FetchCapError | FETCH_CAP_INVALID (403) | Forged, expired, for another CID or namespace |
FetchCapError | FETCH_CAP_REVOKED (403) | The owner revoked it |
FetchCapError | FETCH_CAP_MISSING (401) | No capability header reached the storage node |
ScopeError | FETCH_CAP_REVOKE_KEY_INVALID (403) | revokeFetchCap was given a revokeKey that was not issued for that id (no key at all throws VALIDATION_FAILED before the call) |
RelayError | RELAY_DESTINATION_NOT_ALLOWED (400) | The relay does not serve this namespace host |
RelayError | RATE_LIMITED (429) | Relay limit; retryAfterSeconds says when |
RelayError | RELAY_UNAVAILABLE (503) | The relay cannot carry a stream now |
RelayError | RELAY_CONNECT_FAILED | Relay unreachable, TLS to the namespace host failed, or the stream closed early |
RelayError | RELAY_PROTOCOL_ERROR, RELAY_RESPONSE_TOO_LARGE, RELAY_NONE_CONFIGURED, RELAY_UNSUPPORTED_RUNTIME | Malformed answer; body over maxBodyBytes; no usable relay; not Node |
Network
client.network has health, status, peers, connect, disconnect and proxyAnon (the Tor anonymity proxy; see Tor).
Cancelling work
Every request method takes an AbortSignal. A cancelled request rejects with a NetworkError whose code is ABORTED, is never retried and is not reported through onNetworkError.
Errors
Every failure is an SDKError.
| Class | When | Extra |
|---|---|---|
AuthError | 401: credential missing, expired, or not enough alone | requiredScope |
RevokedCredentialError | 401 AUTH_REVOKED: sign in again, do not retry | |
ScopeError | 403: the credential's grants do not cover the operation | requiredScope |
NamespaceError | 403 NAMESPACE_MISMATCH: wrong gateway, or the key came from the wrong environment | namespace, credentialNamespace |
NotFoundError | 404 | |
NetworkError | No HTTP response. httpStatus is 0 | code is NETWORK_ERROR, TIMEOUT or ABORTED |
error.code is the gateway's own (full list in Sign-in and sessions). error.retryable is true or false when the gateway says, otherwise undefined. Scope and Role are exported types, so a grant name that does not exist is a compile error.
import { NetworkError, ScopeError } from "@debros/orama";
try {
await client.storage.upload(file, file.name);
} catch (error) {
if (error instanceof ScopeError) console.error(`this key needs the ${error.requiredScope} grant`);
else if (error instanceof NetworkError) console.error("the gateway was not reached");
else throw error;
}Grants
import { SCOPES, PROFILE_SCOPES, satisfiesScope } from "@debros/orama";
PROFILE_SCOPES["app-runtime"]; // ["invoke", "storage", "push", "webrtc", "proxy"]
satisfiesScope(["admin"], "storage"); // true: admin is a wildcardThe grants are admin, cache, invoke, proxy, pubsub, push, storage and webrtc. They mirror the gateway's own list, and a test in the SDK fails if the two ever disagree.
Retries and failover
A retryable status (408, 429, 500, 502, 503, 504) is retried up to maxRetries times. A Retry-After header is honoured, capped at thirty seconds; otherwise the delay grows with the attempt and carries up to 25% jitter. Failing over to another gateway is the application's decision, made in onNetworkError:
const client = createClient({
baseURL: gateways[0],
apiKey,
onNetworkError: (error, context) => { if (error.httpStatus === 0) rotateGateway(context.path); },
});Secrets
The vault client is @debros/orama-vault. It talks to guardian daemons on the WireGuard overlay, which an application outside the cluster cannot reach. An application reaches the vault through the gateway's /v1/vault/push and /v1/vault/pull, which do the Shamir split server side and authenticate each request with a per-request Ed25519 ownership signature. See Vault.
A gateway with an untrusted certificate
A test cluster on Let's Encrypt staging presents a certificate your runtime does not trust. Supply a fetch that relaxes verification for that connection only:
import { Agent, fetch as undiciFetch } from "undici";
const dispatcher = new Agent({ connect: { rejectUnauthorized: false } });
const client = createClient({
baseURL, apiKey,
fetch: (input, init) => undiciFetch(input, { ...init, dispatcher }) as unknown as Promise<Response>,
});The SDK never changes the process's TLS settings on your behalf: that would disable verification for every other HTTPS client in the process.
The chain module
@debros/orama/chain reads the Orama chain, builds and signs its transactions, and describes them for an approval screen.
import { LocalSigner, MSG, OramaChainClient, describeTx } from "@debros/orama/chain";
const chain = new OramaChainClient({
gatewayURL: "https://cluster.example.com", // the read-only /v1/chain/ proxy
restURL: "http://127.0.0.1:31003", // a node's REST API: accounts, balances, broadcast
});
const signer = new LocalSigner(privateKeyHex); // or any OramaSigner, such as RootWallet
const msg = MSG.nodesFundHotKey.create({ operator: signer.address, nodeId: "node-1", amount: "2500000000" });
const { signed, result } = await chain.signAndBroadcast([msg], signer, {
chainId: "orama-stagenet-1", gasLimit: 300_000, feeNorama: 300_000,
});
console.log(result.txHash, describeTx(signed.txBytes).messages[0].summary);
// Use earnings: withdraw them to your balance, then pay in the open by saying so.
const tx = { chainId: "orama-stagenet-1", gasLimit: 200_000, feeNorama: 200_000 };
await chain.withdrawEarnings(25_000_000_000n, signer, tx); // 25 ORAMA
const paid = await chain.transfer({ to: recipient, amount: 5_000_000_000n, public: true }, { signer, tx });
console.log(paid.warning); // the sender, the recipient and the amount are visible on the chain- +Reads. The gateway proxy serves
status,block,blocks,tx,validators,supply,stakingPooland the indexer'sindexStatus,indexBlock,indexTx,indexAccountTxs,cnftAsset,cnftOwnerAssets; the two lists take{ page, limit }withpage1 to 10, and past that{ cursor }, thenext_cursorof the previous page (never withpage). A node's REST API addsaccount,balancesandbroadcast. Module state speaks gRPC only, somoduleQuery(service, method, request, { height })reads any query through the gateway, with typed reads forx/nodes(nodesParams,operator,node,nodeCluster,nodeUnbondings,nodeByName,nameOfNode,nodeNames),x/storage(storageParams,deal,slot,storageAuthorization,storageChallenges,storageEpochMint,storageQueue),x/fees(feesParams,baseFee,earnings,feesDeposit),x/archive(archiveParams,archiveRange,lastArchivedHeight,retainHeight) andx/relay(relayParams,relayReporters,relay,relayEpochResult). uint64 fields come back as decimal strings; a key not on chain is anSDKErrorwithhttpStatus404.x/housesand the other modules are reachable throughmoduleQuery, and a read that needs a base URL the config does not have fails and names it. - +A wallet with only a gateway. A wallet shows an address and sends from it through
gatewayURLalone, with no node and norestURL. Balances, delegations and unbonding are wallet queries on/v1/chain/query/:bankBalance,bankAllBalances,bankSpendableBalances,authAccount,authAccountInfo,stakingDelegation,stakingDelegatorDelegations,stakingUnbondingDelegation,stakingDelegatorUnbondingDelegations,stakingValidator,stakingParams,distributionDelegationRewards,distributionDelegationTotalRewards,contractInfoandisContract. ThensimulateTx(txBytes)returns the gas and fee (gasWantedis the limit the transaction declares; both gas figures arebigints),broadcastTx(txBytes)submits and answers once the node's mempool has checked the transaction, andtx(hash)polls until the transaction is in a block (aNotFoundErroruntil then). The listings take{ limit, key }(limit 1 to 100, key the previous page'spagination.next_key).isContractisfalseon the gateway's typed 404 for an address that is not a contract. A transaction the chain refuses, on either call, is aChainTxRefusedError(anSDKErrorwithcodeCHAIN_TX_REJECTED,httpStatus422, andchainCode,codespace,logand, for a broadcast,txHash); thelogis the gateway's sanitised copy of the reason.signAndBroadcaststill usesrestURL. - +Building.
buildSignDocmakes theSIGN_MODE_DIRECTbody, auth info andSignDocfor one signer;signTxhas anOramaSignersign it and checks the signature;verifyTxverifies an encoded transaction the way the chain does.MSGis the message set: 69 messages, 59 of them Orama's own, plus bank send, staking, unjail, reward withdrawal and the two CosmWasm messages a wallet signs. The fee is always norama. - +Sending ORAMA: private by default, public by choice.
chain.transfer({ to, amount }, options)sends privately unless the request sayspublic: true. Only the booleantruepays in the open: an absent orfalseflag is private, and any other value ("false",1,null) is aTypeError, so a transfer cannot turn public by omission or by a mistyped string.amountis a positive whole number of norama (abigint, a safe-integernumberor a decimal string); a fraction or a zero is aRangeError. A private transfer needs a shielded wallet, which proves with the owner's shielded spending key and so never lives in this SDK: pass it asoptions.shielded({ build({ to, amount }) }returns the signer-less transactions in broadcast order, and the SDK broadcasts them throughgatewayURL). Without one the call throwsPrivateTransferUnavailableError(codePRIVATE_TRANSFER_UNAVAILABLE) and sends nothing; it never falls back to a public payment. A public transfer needsoptions.signerandoptions.tx(chainId,gasLimit,feeNorama), signs a bank send, and returns{ privacy: "public", txHash, warning }, wherewarningisPUBLIC_TRANSFER_WARNING: the sender, the recipient and the amount are visible on the chain to everyone, permanently. Show it to the person who chose to pay publicly. If a later transaction of a private transfer fails after earlier ones were broadcast, the error isPRIVATE_TRANSFER_INCOMPLETEand names the hashes already sent. Every broadcast (broadcast,broadcastTx,signAndBroadcast,transfer) computes the transaction's hash itself (txHashOf) and throwsCHAIN_TX_HASH_MISMATCHwhen the node or gateway answers another, and the chain's reason text is stripped of control characters before it reaches an error. RootWallet does not yet build shielded bundles, so today a wallet-driven transfer is a public one. - +Using earnings.
chain.withdrawEarnings(amount, signer, tx)buildsMsgWithdrawEarningsfor the signer: the chain moves that much of the signer's earnings to the signer's own bank balance, and there is no destination parameter. Withdrawing is visible on the chain. From the balance, send it publicly withtransfer({ ..., public: true }), or shield it. - +Signers.
OramaSigneris{ address, publicKey, signDirect(signDoc) }. The builder hands the signer the serializedSignDoc, never a key, so a wallet can decode it and show the user what they approve.LocalSignerholds a key in memory, for tests and servers that own their key. - +Approval text.
describeTxanddescribeMessagesummarize each message, with notes for what a signer must see (a token's powers, a deal allowance's limits and expiry, a collection's royalty, a contract call's decoded body) and asensitiveflag. A message type the SDK does not hold is described as unknown and sensitive.
Generated code and the Go side. src/chain/gen is generated from chain/proto by pnpm gen:chain (protoc and ts-proto, pinned). A test regenerates it and fails on a difference. chain/client/tx/testdata/tx_vectors.json is written by the Go builder: every case is rebuilt in TypeScript and must match the body, auth info, SignDoc, signature and transaction bytes. wallet_msgs.json lists every message the chain registers; the SDK must decode all of them.
Testing against a gateway
export GATEWAY_BASE_URL=https://ns-myapp.cluster.example.com export GATEWAY_API_KEY=<a test key> pnpm test:e2e
Without GATEWAY_API_KEY those tests report as skipped; the unit suite needs nothing.