Getting started

Getting started

This page gets you from “I want to try Junjo” to “I have a working SDK call against a live server” in under a minute. After this, the tutorial walks through creating a group, inviting a user, and assigning a role.

1. Pick a server

Junjo runs as a single binary. Cloud and self-host are the same code; the only difference is who hosts the Postgres database and the Node process.

OptionWhen to pick it
Cloud (managed)You want a hosted endpoint and zero infrastructure.
Self-host (Docker)You want full control, want to avoid a managed dependency, or are running a closed network.
Local dev (this repo)You are evaluating Junjo or developing against a feature branch.

Cloud

Hosted access is by request while Junjo is in beta: email gabecurran01@gmail.com and you will get a managed instance and an API key. Free during the beta. Skip to step 2.

Self-host (Docker)

There is no published container image yet. Build one from the repo’s root Dockerfile (the default WORKSPACE=server build target is the API server); the image applies pending migrations on boot before it starts listening:

git clone https://github.com/GabeCurran/junjo
cd junjo
docker build -t junjo-server .
docker run -e DATABASE_URL=postgres://... -p 8787:8787 junjo-server

If Postgres runs on the host (not in a container), localhost in the connection string points back at the container itself, not the host, so the boot-time migration fails to connect. Reach a host-run database at host.docker.internal on Docker Desktop (-e DATABASE_URL=postgres://user:pass@host.docker.internal:5432/junjo), or run the container with --network host and keep localhost, or use the Compose service name when both run under Compose (see Self-hosting).

Then issue an API key against your running container. The key is shown exactly once and cannot be recovered:

docker exec <container> npm run db:seed --workspace @junjo/server -- --name "My Game"

For a real deployment (Docker Compose recipe, the full env-var table, the migration lifecycle across upgrades, reverse-proxy notes for the SSE path), see the Self-hosting page.

Local dev

The repo bootstraps itself. With Docker running:

git clone https://github.com/GabeCurran/junjo
cd junjo
npm install
npm run dev

npm run dev first runs scripts/ensure-pg.mjs, which starts (or restarts) a junjo-test-pg Postgres container, writes the gitignored root .env with working dev defaults, applies migrations, and seeds a demo game, printing a fresh jk_ API key and persisting it into .env. It then boots the API server (:8787), the dashboard (:3000), and the docs site (:3001). Run npm run dev:server-only for the same bootstrap with just the API server.

If you would rather point at your own Postgres, create the env file yourself first (.env is gitignored, and the server’s dev script reads the root ../../.env at startup), then export DATABASE_URL for the Prisma commands:

cp .env.example .env    # then set DATABASE_URL inside it
export DATABASE_URL=postgres://...
npm run db:migrate --workspace @junjo/server
npm run db:seed --workspace @junjo/server -- --name "My Game"
npm run dev --workspace @junjo/server

On Windows PowerShell, the first two lines differ (cp / export are POSIX; the npm lines are identical):

Copy-Item .env.example .env    # then set DATABASE_URL inside it
$env:DATABASE_URL = "postgres://..."

The seed command prints a prefix.secret API key to stdout. Save it; the secret half is hashed at rest and cannot be recovered.

The default listen port is 8787. Override with PORT=.... See packages/server/README.md for the full env-var table.

2. Install the SDK

npm install @junjo.io/sdk

For React apps, also install the hooks package:

npm install @junjo.io/react

The hooks package depends on @junjo.io/sdk and React 18+. Both packages ship dual ESM and CJS builds, with TypeScript types for each format.

Junjo’s auth adapters live behind a separate import path so non-Clerk / non-Supabase consumers do not pay the dependency cost:

# only if you use the Clerk adapter
npm install @clerk/backend
 
# only if you use the Supabase adapter
npm install @supabase/supabase-js
 
# the JWT adapter has no peer deps

Building on another platform? The Roblox, C++, and Unreal Engine SDKs each have their own install path on their pages; the rest of this page follows the TypeScript SDK.

3. Construct a client

import { Junjo } from "@junjo.io/sdk";
 
const junjo = new Junjo({
  apiKey: process.env.JUNJO_API_KEY!,
  // baseUrl: "http://localhost:8787", // self-host or local dev
});

This pattern is for servers: Node processes, API routes, workers, anywhere process.env.JUNJO_API_KEY stays private. The key is a full-control secret, so browsers must never hold it (no NEXT_PUBLIC_ / VITE_ env vars). A browser client is constructed with new Junjo({ proxy: true, baseUrl: "/api/junjo" }) and your backend injects the key while forwarding; see JunjoProvider + useJunjo for the full proxy setup.

OptionRequiredNotes
apiKeyyes, unless proxyThe full prefix.secret string. Hosted-beta keys arrive by email (there is no beta dashboard login yet); self-host and local dev print one from db:seed. Server-side only.
proxynoBrowser mode: requests go to baseUrl (required) with no credential, and passing apiKey throws. Your backend injects the key. See the React provider page.
baseUrlnoOverride the API root. Defaults to the cloud endpoint. Trailing slashes are stripped.
inviteBaseUrlnoPublic URL prefix used by inviteByLink to build the share URL. No default: without it, inviteByLink throws JunjoError code invalid_config.
authAdapternoAn AuthAdapter (Clerk, Supabase, JWT, or BYO). Server-to-server calls do not need one.
fetchnoOverride the fetch implementation. Useful for tests and for runtimes without a global fetch.
timeoutMsnoPer-request timeout in milliseconds. Defaults to 30000; 0 disables it. SSE subscriptions are exempt. Every request-making method also accepts a per-request override.

The Junjo class is cheap to construct. You can either keep one instance for the lifetime of your process (the common case) or build one per request if you scope the API key per tenant.

4. Make your first call

The fastest sanity-check: list your groups. A fresh project returns an empty page.

const page = await junjo.groups.list();
console.log(page.items);    // []
console.log(page.nextCursor); // null

If this returns without throwing, your API key is good and the server is reachable.

5. Errors

Every SDK method throws JunjoError for any non-2xx response. Branch on error.code (stable) rather than error.message (not stable):

import { JunjoError } from "@junjo.io/sdk";
 
try {
  await junjo.groups.create({ kind: "guild", name: "" });
} catch (e) {
  if (e instanceof JunjoError) {
    e.code;    // "bad_request"
    e.status;  // 400
    e.message; // "name: too short"
  }
}

The full code table lives on the errors reference.

Where to next

  • Tutorial - create a group, invite a user, assign a role, and listen for events.
  • SDK reference - every namespace, every method, every option.
  • React reference - hooks and the provider.
  • Auth adapters - hook Junjo into Clerk, Supabase, or your own JWT issuer.