npm package: @myco-dev/sdk
Description: TypeScript SDK for building apps on the Myco platform.
Latest version: 0.1.14

# @myco/sdk

TypeScript SDK for building apps on the Myco platform.

## Installation

```bash
npm install @myco/sdk
```

### Peer Dependencies

```bash
npm install react better-auth swr
```

## Quick Start

```typescript
import { MycoSDK } from "@myco/sdk";

// Optional: import generated types for type-safe entity access
import type { EntityTypes } from "./types/myco.generated";

const myco = new MycoSDK<EntityTypes>("your-app-key");

// Fetch records - automatically waits for SDK to be ready
const { records } = await myco.data.getRecords("property");
```

## Client SDK

### Initialization

```typescript
import { MycoSDK } from "@myco/sdk";

const myco = new MycoSDK<EntityTypes>("your-app-key", {
  // Optional: override the base URL
  baseUrl: "https://api.myco.com",
  // Optional: disable auto-redirect on unauthenticated (default: true)
  redirectOnUnauth: false,
});

// Optional: await ready if you need to know when initialization completes
// All API calls automatically wait for ready internally
await myco.ready;
```

The SDK automatically:
- Detects local vs production environment
- Handles workspace initialization via the `/api/app/join` endpoint
- Persists workspace ID in localStorage
- Redirects to login if unauthenticated (configurable)

---

### Auth Namespace

#### `myco.auth.login(returnTo?)`

Redirects to the federated login page.

```typescript
// Redirect to login, return to current page after
myco.auth.login();

// Redirect to login, return to specific URL after
myco.auth.login("/dashboard");
```

#### `myco.auth.logout()`

Signs out the current user and clears the session.

```typescript
await myco.auth.logout();
```

#### `myco.auth.useSession()`

React hook for session state.

```typescript
function MyComponent() {
  const { data: session, isPending, error } = myco.auth.useSession();

  if (isPending) return <div>Loading...</div>;
  if (!session) return <div>Not logged in</div>;

  return <div>Hello, {session.user.name}</div>;
}
```

#### `myco.auth.getSession()`

Get session data outside of React components.

```typescript
const session = await myco.auth.getSession();
```

---

### Workspace Namespace

The workspace namespace handles initialization automatically — no manual `ensure()` needed.

#### `myco.workspace.ready`

Promise that resolves when the workspace is initialized. All other SDK namespaces wait for this internally.

```typescript
await myco.workspace.ready;
```

#### `myco.workspace.list()`

List all workspaces the user has access to.

```typescript
const workspaces = await myco.workspace.list();
```

#### `myco.workspace.current()`

Get the current workspace details.

```typescript
const workspace = await myco.workspace.current();
console.log(workspace.name);
```

#### `myco.workspace.switch(workspaceId)`

Switch to a different workspace.

```typescript
myco.workspace.switch("ws_abc123");
```

#### `myco.workspace.update(data)`

Update the current workspace.

```typescript
await myco.workspace.update({ name: "Renamed Workspace" });
```

#### `myco.workspace.useWorkspace()`

React hook for the current workspace.

```typescript
function WorkspaceHeader() {
  const { data: workspace, isPending, error } = myco.workspace.useWorkspace();

  if (isPending) return <div>Loading...</div>;

  return <h1>{workspace?.name}</h1>;
}
```

#### `myco.workspace.members.get()`

Get members of the current workspace.

```typescript
const members = await myco.workspace.members.get();
```

#### `myco.workspace.members.add(userId, role?)`

Add a member to the current workspace.

```typescript
await myco.workspace.members.add("user_123", "admin");
await myco.workspace.members.add("user_456"); // defaults to "member"
```

#### `myco.workspace.members.remove(userId)`

Remove a member from the current workspace.

```typescript
await myco.workspace.members.remove("user_123");
```

---

### Data Namespace

#### Entities

##### `myco.data.getEntities()`

Get all entities for the current app.

```typescript
const entities = await myco.data.getEntities();
```

##### `myco.data.getEntity(entitySlug)`

Get a single entity by slug.

```typescript
const entity = await myco.data.getEntity("property");
console.log(entity.fields);
```

#### Records (Async Methods)

##### `myco.data.getRecords(entitySlug, options?)`

Get paginated records for an entity.

```typescript
const { records, pagination } = await myco.data.getRecords("property", {
  page: 1,
  pageSize: 20,
  sort: "createdAt",
  order: "desc",
  filter: { status: "active" },
});
```

##### `myco.data.getRecord(entitySlug, recordId)`

Get a single record by ID.

```typescript
const record = await myco.data.getRecord("property", "rec_123");
```

##### `myco.data.createRecord(entitySlug, data)`

Create a new record.

```typescript
const record = await myco.data.createRecord("property", {
  title: "Beach House",
  price: 500000,
});
```

##### `myco.data.updateRecord(entitySlug, recordId, data)`

Update an existing record.

```typescript
const record = await myco.data.updateRecord("property", "rec_123", {
  price: 550000,
});
```

##### `myco.data.deleteRecord(entitySlug, recordId)`

Delete a record.

```typescript
await myco.data.deleteRecord("property", "rec_123");
```

##### `myco.data.batchGetRecords(entitySlug, ids)`

Batch get multiple records by IDs.

```typescript
const records = await myco.data.batchGetRecords("property", [
  "rec_123",
  "rec_456",
]);
```

#### Records (React Hooks)

##### `myco.data.useRecords(entitySlug, options?)`

SWR hook for fetching records with built-in pagination.

```typescript
function PropertyList() {
  const {
    data: properties,
    isLoading,
    error,
    // Pagination
    page,
    hasNextPage,
    hasPreviousPage,
    nextPage,
    previousPage,
    setPage,
  } = myco.data.useRecords("property", {
    pageSize: 10,
    initialPage: 1,
    sort: "createdAt",
    order: "desc",
    filter: { status: "active" },
    // Called when page changes (for URL sync, analytics, etc.)
    onPageChange: (page) => {
      router.push(`?page=${page}`);
    },
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      {properties?.map((property) => (
        <div key={property.id}>{property.data.title}</div>
      ))}
      <button onClick={previousPage} disabled={!hasPreviousPage}>
        Previous
      </button>
      <span>Page {page}</span>
      <button onClick={nextPage} disabled={!hasNextPage}>
        Next
      </button>
    </div>
  );
}
```

**Options:**

| Option | Type | Description |
|--------|------|-------------|
| `initialPage` | `number` | Starting page (default: 1) |
| `pageSize` | `number` | Records per page |
| `sort` | `string` | Field to sort by |
| `order` | `"asc" \| "desc"` | Sort direction |
| `filter` | `object` | Filter criteria |
| `onPageChange` | `(page: number) => void` | Callback when page changes |

**Return value extends SWR response with:**

| Property | Type | Description |
|----------|------|-------------|
| `page` | `number` | Current page number |
| `hasNextPage` | `boolean` | Whether there's a next page |
| `hasPreviousPage` | `boolean` | Whether there's a previous page |
| `nextPage` | `() => void` | Go to next page |
| `previousPage` | `() => void` | Go to previous page |
| `setPage` | `(page: number) => void` | Jump to specific page |

##### `myco.data.useRecord(entitySlug, recordId)`

SWR hook for fetching a single record.

```typescript
function PropertyDetail({ id }: { id: string }) {
  const { data: property, isLoading } = myco.data.useRecord("property", id);

  if (isLoading) return <div>Loading...</div>;

  return <div>{property?.data.title}</div>;
}
```

Supports conditional fetching by passing `null` or `undefined`:

```typescript
// Only fetch when selectedId is set
const { data } = myco.data.useRecord("property", selectedId ?? null);
```

---

## Server SDK

For server-side rendering and API routes (e.g., Remix loaders/actions).

```typescript
import { createServerSDK } from "@myco/sdk/server";

export async function loader({ request }: LoaderArgs) {
  const myco = createServerSDK(
    { MYCO_APP_KEY: process.env.MYCO_APP_KEY! },
    request
  );

  const { user } = await myco.auth.getVerifiedUser();

  if (!user) {
    return redirect(myco.auth.getLoginUrl(request.url));
  }

  const { records } = await myco.data.getRecords("property");
  return json({ records });
}
```

### Server Auth

#### `myco.auth.getVerifiedUser()`

Verify the JWT from cookies and return the user.

```typescript
const { user, jwt } = await myco.auth.getVerifiedUser();
if (!user) {
  // Not authenticated
}
```

#### `myco.auth.getLoginUrl(returnTo)`

Get the federated login URL.

```typescript
const loginUrl = myco.auth.getLoginUrl("https://myapp.com/dashboard");
return redirect(loginUrl);
```

### Server Workspace

Same API as client SDK (minus `switch`, `useWorkspace`, and `ready`):
- `myco.workspace.list()`
- `myco.workspace.current()`
- `myco.workspace.update(data)`
- `myco.workspace.members.get()`
- `myco.workspace.members.add(userId, role?)`
- `myco.workspace.members.remove(userId)`

### Server Data

Same API as client SDK (async methods only, no hooks):
- `myco.data.getEntities()`
- `myco.data.getEntity(entitySlug)`
- `myco.data.getRecords(entitySlug, options?)`
- `myco.data.getRecord(entitySlug, recordId)`
- `myco.data.createRecord(entitySlug, data)`
- `myco.data.updateRecord(entitySlug, recordId, data)`
- `myco.data.deleteRecord(entitySlug, recordId)`
- `myco.data.batchGetRecords(recordIds)`

### Server Fetch

For custom API calls:

```typescript
const result = await myco.fetch<MyType>("/api/custom-endpoint", {
  method: "POST",
  body: JSON.stringify({ data }),
});
```

---

## Error Handling

The SDK exports an `ApiError` class for handling API errors:

```typescript
import { ApiError } from "@myco/sdk";

try {
  await myco.data.getRecord("property", "invalid-id");
} catch (error) {
  if (error instanceof ApiError) {
    console.log(error.status); // HTTP status code
    console.log(error.body);   // Response body
    console.log(error.path);   // Request path
  }
}
```

---

## TypeScript

### Generated Types

For type-safe entity access, generate types from your Myco schema:

```typescript
// types/myco.generated.ts
export interface EntityTypes {
  property: {
    title: string;
    price: number;
    bedrooms: number;
    status: "active" | "sold";
  };
  agent: {
    name: string;
    email: string;
  };
}
```

Then pass the type to `MycoSDK`:

```typescript
import type { EntityTypes } from "./types/myco.generated";

const myco = new MycoSDK<EntityTypes>("your-app-key");

// Now fully typed!
const { records } = await myco.data.getRecords("property");
// records is TypedEntityRecord<{ title: string; price: number; ... }>[]
```

### Exported Types

```typescript
import type {
  MycoSDKConfig,
  JoinResponse,
  VerifiedUser,
  Workspace,
  WorkspaceMember,
  Entity,
  EntityField,
  EntityRecord,
  PaginatedResponse,
  GetRecordsOptions,
  TypedEntityRecord,
} from "@myco/sdk";
```