Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,10 @@ AUTH_REDIRECT_PROXY_URL=""
# its "connect" state instead (the bridge answers 501 bridge_misconfigured).
#
# The `b44k_` workspace API key used on the hot path: minting. Needs
# `user_tokens:mint`. Sent in Authorization *bare*, no "Bearer". The most
# `user_tokens:mint` and `service_users:provision`. Sent in Authorization *bare*, no "Bearer". The most
# sensitive value here — it can vend a token acting as any service principal in
# the workspace.
BASE44_SVC_KEY=""
# Optional second key for the two privileged calls, provision + deprovision
# (`service_users:provision`). Defaults to BASE44_SVC_KEY when unset, which is
# fine to start with. Splitting them is the stronger posture: a mint-only key on
# the hot path is what makes deprovisioning actually stick, because a
# provision-capable key lets a removed user press Connect and come straight back.
BASE44_PROVISION_KEY=""
# Not secrets — the workspace and folder ids, and the platform host. In env
# because the host rotates and the ids become per-tenant later. Never
# caller-supplied: a request-controlled host here would be an SSRF, and a
Expand Down
3 changes: 3 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@
# at a private registry, which npm would substitute for the lockfile host and then
# fail on. See docs/deploy.md.
registry=https://registry.npmjs.org

# First-party SDK previews are exempt; other packages retain their configured cooldown.
min-release-age-exclude[]=@base44-preview/sdk
8 changes: 6 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ Next.js App Router · TypeScript · Tailwind 4 · Postgres + Prisma · NextAuth
*only* module that queries owner-scoped models — never query them raw. This is the single biggest
correctness risk in the codebase, and ESLint bans by-id `update`/`delete` on those models to keep
it that way.
- **`src/lib/base44Link.ts` is the only module that touches `Base44Link`**, and it never returns a
token to a caller. Vended tokens stay server-side. The webhook receiver joins an event to a user
- **`src/lib/base44Link.ts` and `src/lib/base44TokenStore.ts` own `Base44Link` persistence.**
The first handles connection lifecycle; the second implements the SDK token store.
Tokens stay server-side; browser responses use `linkStatus()` to expose only connection status. The webhook receiver joins an event to a user
through `emailForServiceExternalId()` there, which returns an email and nothing else.
- **An inbound webhook is untrusted until its signature verifies.** `src/lib/base44WebhookSignature.ts`
is the boundary: the URL is public, so the event type, the app id and above all
Expand Down Expand Up @@ -77,6 +78,9 @@ Next.js App Router · TypeScript · Tailwind 4 · Postgres + Prisma · NextAuth
- **Two lint regimes.** Platform infrastructure (`src/lib`, `src/app`) is strict `.tsx`/`.ts`. The
example product UI (`src/components`, `src/views`) is `.jsx` with relaxed lint — it's the example,
not the lesson.
- This is a getting-started guide: optimize integration code for human readers. Use descriptive
names, explicit control flow, and one meaningful operation per statement. Keep SDK usage visible;
separate storage details from the connection flow instead of compressing them together.
- Comments explain what the code *is*, not what it used to be.

## Checks
Expand Down
43 changes: 24 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,11 @@ You need, from Base44:
| --- | --- |
| An **enterprise workspace** with the platform capability enabled | All your users' apps live in it, so you have one place to govern and offboard |
| Its **workspace id** | Sent as `X-Active-Workspace-Id` on every platform call |
| A **workspace API key** (`b44k_…`) with the `user_tokens:mint` scope | Vends per-user access tokens |
| Optionally a **second key** with `service_users:provision` | Creates the per-user identities. Splitting the two is the safer setup — see [step 2](#step-2--give-each-user-their-own-base44-identity) |
| A **workspace API key** (`b44k_…`) with `user_tokens:mint` and `service_users:provision` | Provisions users and vends their server-side tokens |
| The **platform host** your workspace is served from | Base of every REST call |
| An **app folder id** | The one folder your platform files its apps into, so listing apps means listing that folder |

Those land in env as `BASE44_ORG_ID`, `BASE44_SVC_KEY`, `BASE44_PROVISION_KEY`,
Those land in env as `BASE44_ORG_ID`, `BASE44_SVC_KEY`,
`BASE44_PLATFORM_HOST`, `BASE44_APPS_FOLDER_ID` (see `.env.example`). All server-only — none of
them may ever reach the browser.

Expand Down Expand Up @@ -165,21 +164,21 @@ No OAuth redirect, no consent screen, no PKCE — you already own both sides. Th
from the *key*, never from the request; that's your cross-tenant guarantee.

**Mint never auto-provisions.** An unknown principal is a 404, and that's the feature: it's what
makes removing someone actually stick. Which is also why the two scopes are worth splitting —
if the hot-path key can provision, a removed user just presses "Connect" and walks back in.
keeps ordinary requests from recreating a removed identity. The SDK uses one key, so your own
server must prevent offboarded users from invoking the explicit Connect/onboarding action again.

### 2c. Store it, refresh it, and never return it

```ts
// src/lib/base44Link.ts is the ONLY module that reads or writes tokens,
// and no function in it returns one to a caller:
// Browser responses expose connection status, never stored credentials.
export function linkStatus(link) {
return { linked: …, base44_user_email: …, organization_id: … }; // booleans and display fields
}
```

Tokens live ~1h, so re-minting is routine. This repo re-mints proactively 5 minutes before expiry
and once more on a mid-call 401, then gives up and asks the user to reconnect. Note the distinction
through the SDK. Non-chat calls do not automatically retry a mutation after a 401; they clear
the rejected credential and ask the user to reconnect. Legacy chat retains its single retry. Note the distinction
that matters: a **429 or 5xx is a blip** (leave the row alone and retry), a **4xx is a dead grant**
(downgrade to `pending` and show the Connect button). Conflating them turns a busy minute into a
fleet-wide forced reconnect.
Expand All @@ -196,19 +195,25 @@ Your frontend must never hold a Base44 credential. So it calls *you*, and you ca
browser → POST /api/base44/platform {action, …params} → your server → Base44 REST
```

`src/app/api/base44/platform/route.ts` is that proxy. Its design is a single table of allowed
actions — the caller names an action, never a URL:
`src/app/api/base44/platform/route.ts` is that proxy. It dispatches provisioning and non-chat
app operations through the [Platform SDK](https://github.com/base44/javascript-sdk/blob/28beeea/platform-docs/README.md), imported from
`@base44/sdk/platform/server`. Its [reference](https://github.com/base44/javascript-sdk/blob/28beeea/platform-docs/api.md) documents every input, response,
error and token-storage shape. Chat remains on its existing HTTP implementation.

```ts
const OPS = {
listApps: { method: "GET", path: (p) => `/api/apps?…folder_id=${appsFolderId()}` },
createApp: { method: "POST", path: () => "/api/apps", body: (p) => ({ … }) },
sendMessage:{ method: "POST", path: (p) => `/api/apps/${p.appId}/chat/message`, … },
};
```
The dependency is currently pinned to `@base44-preview/sdk@0.8.48-pr.283.2de4c8a`
using an npm alias named `@base44/sdk`. After the platform entry point is released,
replace that alias with the stable SDK version; the imports stay the same. The
SDK's own tests and reference documentation live in the JavaScript SDK repository.
Run `npm run platform-sdk:test` here to verify Sunny's storage and app adapters
against the installed package without calling live services.

Read the integration in this order:

1. `getPlatformClient()` and `connect()` in `src/lib/base44Link.ts`: configure the SDK, provision a user, and record their connection.
2. `src/lib/base44AppOperations.ts`: call the SDK as that user and map results to Sunny’s browser contract.
3. `src/lib/base44TokenStore.ts`: persist credentials using Sunny’s database when the default in-memory store is insufficient.

Nine actions, and that's the whole surface. Why an allow-list and not a passthrough: Base44 enforces
The caller still names an allowlisted action, never a URL. Why an allow-list and not a passthrough: Base44 enforces
OAuth scopes in its MCP tool layer, *not* on this REST surface, so `apps:read apps:write` does not
constrain what a token can do here. **Your allow-list is the actual limit.** Never let a caller
supply a path, a host, or a workspace id.
Expand Down
44 changes: 24 additions & 20 deletions docs/base44-identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
How your platform acts on Base44 *as each of your own users*. This is step 2 of the
[README](../README.md) walkthrough, in full.

Reference implementation: `src/lib/base44Link.ts` (the only module that reads or writes tokens) and
`src/app/api/base44/link/route.ts` (the three-action route in front of it).
SDK: [public contract](https://github.com/base44/javascript-sdk/blob/28beeea/platform-docs/api.md).

Starter adapters: `src/lib/base44Link.ts` handles connection lifecycle,
`src/lib/base44TokenStore.ts` implements persistent SDK token storage, and
`src/app/api/base44/link/route.ts` exposes the three connection actions.

---

Expand Down Expand Up @@ -80,7 +83,7 @@ format Base44's workspace-key auth accepts; `Bearer` is for the *minted* tokens.

```http
POST {BASE44_PLATFORM_HOST}/api/service/users
Authorization: {BASE44_PROVISION_KEY}
Authorization: {BASE44_SVC_KEY}
Content-Type: application/json

{ "service_external_id": "sunny-9f2c…", "display_name": "Sunny user 9f2c…" }
Expand Down Expand Up @@ -157,7 +160,7 @@ prefixes (`chatgpt_`, `claude_`, `cursor_`, `oauth_`) is rejected everywhere exc

```http
DELETE {BASE44_PLATFORM_HOST}/api/service/users/{service_external_id}
Authorization: {BASE44_PROVISION_KEY}
Authorization: {BASE44_SVC_KEY}
```

Scope: `service_users:provision`. Idempotent — a `404` is a no-op.
Expand All @@ -171,18 +174,18 @@ this repo it's exported (`deprovisionPrincipal()`) but deliberately not wired to

---

## Why the two keys are worth splitting

`BASE44_SVC_KEY` (mint) sits on the hot path — every re-mint, every hour, every active user. The
provision key is used twice in an account's lifetime.
## One SDK key

A mint-only key can vend tokens for principals that already exist but cannot *create* one, so it can
never become an impersonate-anyone primitive. And it's what makes deprovision stick: with a
provision-capable key on the hot path, a removed user presses Connect, gets re-provisioned, and the
offboarding quietly undoes itself.
The Platform SDK uses one key with both `service_users:provision` and `user_tokens:mint`.
Existing deployments using a separate provisioning key must add provisioning permission
to `BASE44_SVC_KEY` before deploying this migration. The separate provisioning variable
is no longer read. Your server must prohibit offboarded users from explicitly connecting
again; SDK token acquisition never auto-provisions.

`BASE44_PROVISION_KEY` defaults to `BASE44_SVC_KEY` when unset, so a single-key deployment works.
Split them when you go to production.
The SDK owns acquisition, renewal and revocation. `src/lib/base44TokenStore.ts`
adapts existing rows for persistence. `src/lib/base44Link.ts` keeps the connection
flow, principal IDs, and webhook lookup.
See [the complete SDK storage contract](https://github.com/base44/javascript-sdk/blob/28beeea/platform-docs/tokens.md).

---

Expand All @@ -193,7 +196,7 @@ connect provision (idempotent) ─► mint ─► store row {status: linked
▲ order matters: mint 404s on an unknown principal and will not create one

use expiry within 5 min? ─► re-mint, then call
mid-call 401? ─► re-mint once, retry once, else 428 reauthorize_required
mid-call 401? ─► app SDK calls: clear + 428; legacy chat: one re-mint/retry

disconnect revoke refresh token (best effort) ─► delete the row
✗ does NOT deprovision — the principal owns the user's apps
Expand Down Expand Up @@ -233,11 +236,12 @@ as a configuration error, not get caught and mislabelled as an upstream blip.

The rules this repo holds itself to, all asserted by `npm run base44:smoke`:

1. **One module touches tokens.** `src/lib/base44Link.ts`. The generic entity CRUD refuses the
`Base44Link` model outright, so no API can read it by accident.
2. **No function returns a token.** `linkStatus()` returns `{linked, base44_user_email,
organization_id}` — booleans and display fields. A token leaves the module only as the
`Authorization` header of a server-side fetch.
1. **Token persistence stays in the server adapters.** Only `src/lib/base44Link.ts` and
`src/lib/base44TokenStore.ts` access connection rows. The generic entity CRUD refuses
the `Base44Link` model outright, so no API can read it by accident.
2. **No browser response contains a service token.** `linkStatus()` returns `{linked,
base44_user_email, organization_id}`. The SDK and legacy chat use stored credentials
only on the server.
3. **Everything is keyed by the session email**, taken from the session and never from the request
body. A user cannot connect, inspect or disconnect anyone else's link.
4. **The principal id sent upstream is opaque and never an email.**
Expand Down
7 changes: 7 additions & 0 deletions docs/base44-platform-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ const OPS: Record<string, Op> = { listApps: {…}, createApp: {…}, … };

## The endpoints

Non-chat requests below are implemented by the [Platform SDK](https://github.com/base44/javascript-sdk/blob/28beeea/platform-docs/README.md).
The SDK projects documented camelCase results; the starter adapter maps these into the
existing browser response names. It excludes internal app fields and returns only
`has_custom_instructions` for the creation check, never the instruction contents.
Non-chat calls do not automatically retry after a mutation failure.


Base URL is your platform host. Every call carries the two headers above.

### `GET /api/apps` — list
Expand Down
3 changes: 1 addition & 2 deletions docs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,7 @@ one is.
| `NEXTAUTH_URL` | `https://<your-host>` — the production domain, exactly. **Scope it to production**: it overrides the origin read off the request, so a preview holding it would sign users into production |
| `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` | the production OAuth client |
| `AUTH_REDIRECT_PROXY_URL` | `https://<your-host>/api/auth` — only needed if you turn previews on; set it on production *and* the preview contexts |
| `BASE44_SVC_KEY` | the `b44k_` workspace key — without it the builder shows its "Connect" state |
| `BASE44_PROVISION_KEY` | optional second key; defaults to `BASE44_SVC_KEY` |
| `BASE44_SVC_KEY` | one `b44k_` key with `user_tokens:mint` and `service_users:provision` — without it the builder shows its "Connect" state |
| `BASE44_ORG_ID`, `BASE44_PLATFORM_HOST`, `BASE44_APPS_FOLDER_ID` | from your workspace |
| `NEXT_PUBLIC_BASE44_APP_HOST` | the host Base44 serves built apps from. The one public var — these URLs are built in the browser to be iframed. Unset, the UI shows no app previews |

Expand Down
Loading