Skip to content

Commit 0522331

Browse files
Add reference documentation for Databuddy SDK and core packages
Co-authored-by: eassanassar <[email protected]>
1 parent c61124e commit 0522331

File tree

10 files changed

+580
-0
lines changed

10 files changed

+580
-0
lines changed

docs/reference/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
## Reference Documentation
2+
3+
- **SDK**: [sdk.md](./sdk.md)
4+
- **HTTP API**: [api.md](./api.md)
5+
- **Validation**: [validation.md](./validation.md)
6+
- **Shared Utilities**: [shared.md](./shared.md)
7+
- **RPC**: [rpc.md](./rpc.md)
8+
- **Auth**: [auth.md](./auth.md)
9+
- **Redis & Caching**: [redis.md](./redis.md)
10+
- **Database**: [db.md](./db.md)
11+
- **Event Mapper**: [mapper.md](./mapper.md)

docs/reference/api.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
## HTTP API
2+
3+
Base URL examples:
4+
- Production: `https://api.databuddy.cc`
5+
- Local dev: your `apps/api` server (default port 3001)
6+
7+
Authentication:
8+
- Most routes require either a logged-in session cookie or an `x-api-key` header scoped to the website.
9+
10+
### Health
11+
- **GET** `/health`
12+
- Returns service status for ClickHouse, Database, and Redis.
13+
- Example:
14+
```bash
15+
curl -s https://api.databuddy.cc/health | jq
16+
```
17+
18+
### Query API (analytics)
19+
- **Prefix**: `/v1/query`
20+
- Applies website authentication middleware. If `website_id` is required and the site is not public, you must be authenticated (session or `x-api-key`).
21+
22+
- **GET** `/v1/query/types?include_meta=true`
23+
- Returns available query types and configuration (allowed filters, defaults, optional meta).
24+
- Example:
25+
```bash
26+
curl -s "https://api.databuddy.cc/v1/query/types?include_meta=true" \
27+
-H "cookie: ...session..." | jq
28+
```
29+
30+
- **POST** `/v1/query/compile`
31+
- Validates and compiles a query request without executing it.
32+
- Body fields:
33+
- `projectId` (string), `type` (string), `from` (ISO date), `to` (ISO date)
34+
- Optional: `timeUnit`, `filters[]`, `groupBy[]`, `orderBy`, `limit`, `offset`
35+
- Example:
36+
```bash
37+
curl -s -X POST https://api.databuddy.cc/v1/query/compile?website_id=WEBSITE_ID \
38+
-H "content-type: application/json" \
39+
-H "cookie: ...session..." \
40+
-d '{
41+
"projectId": "WEBSITE_ID",
42+
"type": "summary_overview",
43+
"from": "2025-01-01",
44+
"to": "2025-01-31",
45+
"timeUnit": "day",
46+
"limit": 100
47+
}' | jq
48+
```
49+
50+
- **POST** `/v1/query`
51+
- Executes one or more dynamic query requests. Supports batch input (array).
52+
- Body (single request):
53+
- `parameters`: Array of strings or objects `{ name, start_date?, end_date?, granularity?, id? }`
54+
- Optional: `limit`, `page`, `filters[]`, `granularity`, `groupBy`, `startDate`, `endDate`, `timeZone`
55+
- Example (single):
56+
```bash
57+
curl -s -X POST "https://api.databuddy.cc/v1/query?website_id=WEBSITE_ID" \
58+
-H "content-type: application/json" \
59+
-H "x-api-key: YOUR_WEBSITE_API_KEY" \
60+
-d '{
61+
"parameters": ["summary_overview"],
62+
"limit": 50,
63+
"granularity": "daily"
64+
}' | jq
65+
```
66+
- Example (batch):
67+
```bash
68+
curl -s -X POST "https://api.databuddy.cc/v1/query?website_id=WEBSITE_ID" \
69+
-H "content-type: application/json" \
70+
-H "cookie: ...session..." \
71+
-d '[
72+
{ "parameters": ["summary_overview"], "granularity": "daily" },
73+
{ "parameters": [{ "name": "pages_top", "start_date": "2025-01-01", "end_date": "2025-01-31" }] }
74+
]' | jq
75+
```
76+
77+
### Assistant API (streaming)
78+
- **Prefix**: `/v1/assistant`
79+
- **POST** `/v1/assistant/stream`
80+
- Streams assistant responses for a given website and message.
81+
- Body: `{ message: string, website_id: string, model?: 'chat'|'agent'|'agent-max', context?: { previousMessages?: { role?: string, content: string }[] } }`
82+
- Example (fetch streaming):
83+
```ts
84+
async function streamAssistant() {
85+
const res = await fetch('https://api.databuddy.cc/v1/assistant/stream?website_id=WEBSITE_ID', {
86+
method: 'POST',
87+
headers: { 'content-type': 'application/json', 'cookie': '...session...' },
88+
body: JSON.stringify({ message: 'Show sessions trend', website_id: 'WEBSITE_ID' })
89+
});
90+
const reader = res.body!.getReader();
91+
const decoder = new TextDecoder();
92+
while (true) {
93+
const { value, done } = await reader.read();
94+
if (done) break;
95+
console.log(decoder.decode(value)); // streaming updates
96+
}
97+
}
98+
```
99+
100+
### Authentication Notes
101+
- Routes enforce website access using either session cookies or `x-api-key` with appropriate scope. Public websites bypass auth for read access.

docs/reference/auth.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
## @databuddy/auth
2+
3+
### Overview
4+
- Wraps `better-auth` with Drizzle adapter, email providers, OTP, magic links, organizations, and custom session fields.
5+
- Exports:
6+
- `auth`: Better Auth instance (`auth.api.getSession({ headers })`, etc.)
7+
- `websitesApi`: `{ hasPermission: auth.api.hasPermission }`
8+
- Types: `User`, `Session`
9+
- Permissions/roles helpers
10+
11+
### Getting the current session (server)
12+
```ts
13+
import { auth } from '@databuddy/auth';
14+
15+
const session = await auth.api.getSession({ headers: request.headers });
16+
if (!session?.user) {
17+
// not authenticated
18+
}
19+
```
20+
21+
### Checking website permissions
22+
```ts
23+
import { websitesApi } from '@databuddy/auth';
24+
25+
const { success } = await websitesApi.hasPermission({
26+
headers: request.headers,
27+
body: { permissions: { website: ['read'] } },
28+
});
29+
```
30+
31+
### Notes
32+
- The package configures email delivery using Resend and sends verification, magic link, OTP, and invitation emails.
33+
- Sessions are cached with Redis for performance.

docs/reference/db.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
## @databuddy/db
2+
3+
### Overview
4+
- Exposes Drizzle ORM (`export * from 'drizzle-orm'`), a configured Postgres client `db`, and ClickHouse client/schema exports.
5+
- Also exports all Drizzle `schema` and `relations` so you can write typed queries.
6+
7+
### Usage
8+
```ts
9+
import { db, eq, websites } from '@databuddy/db';
10+
11+
const list = await db.query.websites.findMany({
12+
where: eq(websites.userId, userId),
13+
limit: 20,
14+
});
15+
```
16+
17+
### Environment
18+
- Requires `DATABASE_URL` to be set.

docs/reference/mapper.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
## @databuddy/mapper
2+
3+
### Overview
4+
- Normalizes third-party analytics export rows into Databuddy `AnalyticsEvent` objects.
5+
- Exports:
6+
- `mapEvents(adapter, rows)`
7+
- `adapters.umami(clientId)` and `AnalyticsEventAdapter` type
8+
9+
### Umami Adapter
10+
```ts
11+
import { adapters, mapEvents } from '@databuddy/mapper';
12+
13+
const adapter = adapters.umami('YOUR_CLIENT_ID');
14+
const events = mapEvents(adapter, umamiCsvRows);
15+
// events: AnalyticsEvent[] ready for ingestion
16+
```
17+
18+
The adapter maps fields like `session_id`, `distinct_id`, geo, screen, UTM fields, page title, and timestamps to Databuddy event structure.

docs/reference/redis.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
## @databuddy/redis
2+
3+
### Redis Client
4+
- `redis`: a singleton ioredis client (JSON helpers included)
5+
- `getRedisCache()`: returns the singleton client (throws if `REDIS_URL` missing)
6+
- `getRawRedis()`: returns a non-extended raw client
7+
- `getJson<T>(key): Promise<T|null>` and `setJson<T>(key, value, expireInSec)` on the extended client
8+
- Lock helpers: `getLock(key, value, timeoutMs)`, `releaseLock(key, value)`, `isRedisConnected()`, `disconnectRedis()`
9+
10+
```ts
11+
import { redis, getLock, releaseLock } from '@databuddy/redis';
12+
13+
await redis.set('hello', 'world');
14+
const got = await redis.get('hello');
15+
16+
const ok = await getLock('my-lock', 'job-123', 10_000);
17+
if (ok) {
18+
try {
19+
// do work
20+
} finally {
21+
await releaseLock('my-lock', 'job-123');
22+
}
23+
}
24+
```
25+
26+
### cacheable(fn, options)
27+
- Memoizes async functions in Redis with SWR support.
28+
- Options: `expireInSec`, `prefix?`, `serialize?`, `deserialize?`, `staleWhileRevalidate?`, `staleTime?`, `maxRetries?`.
29+
- Returns a wrapped function with helpers: `.getKey(...)`, `.clear(...)`, `.clearAll()`, `.invalidate(...)`.
30+
31+
```ts
32+
import { cacheable } from '@databuddy/redis';
33+
34+
const getUser = cacheable(async (id: string) => {
35+
// fetch from DB
36+
return { id, name: 'Ada' };
37+
}, { expireInSec: 300, prefix: 'user', staleWhileRevalidate: true, staleTime: 30 });
38+
39+
const u = await getUser('123');
40+
await getUser.clear('123');
41+
```
42+
43+
### getCache(key, options, fn)
44+
- Low-level helper to read-through cache a promise with SWR.
45+
46+
```ts
47+
import { getCache } from '@databuddy/redis';
48+
49+
const data = await getCache('stats:today', { expireInSec: 60 }, async () => {
50+
// compute expensive stats
51+
return { sessions: 42 };
52+
});
53+
```
54+
55+
### Drizzle Cache
56+
- `createDrizzleCache({ redis, namespace? })` provides:
57+
- `withCache({ key, ttl?, tables?, tag?, autoInvalidate?, queryFn })`
58+
- `invalidateByTables(tables: string[])`, `invalidateByTags(tags: string[])`, `invalidateByKey(key: string)`
59+
- `cleanupDeps()`
60+
61+
Example:
62+
```ts
63+
import { createDrizzleCache, redis } from '@databuddy/redis';
64+
65+
const websiteCache = createDrizzleCache({ redis, namespace: 'websites' });
66+
const site = await websiteCache.withCache({
67+
key: 'getById:abc',
68+
ttl: 600,
69+
tables: ['websites'],
70+
queryFn: () => db.query.websites.findFirst({ where: eq(websites.id, 'abc') })
71+
});
72+
```

docs/reference/rpc.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
## @databuddy/rpc
2+
3+
### Overview
4+
- Uses tRPC for type-safe server procedures.
5+
- Exports:
6+
- `appRouter`, `type AppRouter`
7+
- `createTRPCContext`, `createTRPCRouter`, `publicProcedure`, `protectedProcedure`, `rateLimitedProtectedProcedure`, `rateLimitedAdminProcedure`
8+
- `getRateLimitIdentifier`, `rateLimiters`
9+
10+
### Context
11+
```ts
12+
import { createTRPCContext } from '@databuddy/rpc';
13+
14+
// Called per request (headers required)
15+
const ctx = await createTRPCContext({ headers: request.headers });
16+
// ctx = { db, auth, session, user, headers }
17+
```
18+
19+
### Routers
20+
```ts
21+
import { createTRPCRouter, publicProcedure, protectedProcedure } from '@databuddy/rpc';
22+
23+
export const exampleRouter = createTRPCRouter({
24+
hello: publicProcedure.query(() => 'world'),
25+
me: protectedProcedure.query(({ ctx }) => ctx.user),
26+
});
27+
```
28+
29+
The `appRouter` mounts feature routers like `websites`, `funnels`, `preferences`, `goals`, `autocomplete`, `apikeys`, `experiments`.
30+
31+
### Rate Limiting Utilities
32+
- `getRateLimitIdentifier(userId?: string, headers?: Headers): string`
33+
- `RateLimiter` class with methods:
34+
- `checkLimit(identifier: string): Promise<{ success; limit; remaining; reset; }>`
35+
- `getStatus(identifier: string)`
36+
- `reset(identifier: string)`
37+
- Built-in `rateLimiters`: `api`, `auth`, `expensive`, `admin`, `public`.
38+
39+
Use within procedures via `rateLimitedProtectedProcedure` or in HTTP middleware (see `apps/api`).
40+
41+
### Referrer Utilities
42+
- `parseReferrer(referrerUrl: string | null | undefined, currentDomain?: string): { type; name; url; domain }`
43+
- `categorizeReferrer(info): string`
44+
- `isInternalReferrer(referrerUrl: string, websiteHostname?: string): boolean`
45+
46+
Example:
47+
```ts
48+
import { parseReferrer, categorizeReferrer } from '@databuddy/rpc/utils/referrer';
49+
50+
const info = parseReferrer('https://www.google.com/search?q=databuddy', 'example.com');
51+
// { type: 'search', name: 'www.google.com', ... }
52+
const category = categorizeReferrer(info); // 'Search Engine'
53+
```
54+
55+
### Auth Utilities
56+
- `authorizeWebsiteAccess(ctx, websiteId, permission: 'read'|'update'|'delete'|'transfer')`
57+
- Ensures the current user (from tRPC context) has access to the given website, considering public sites, owner, admin, or org permissions; throws `TRPCError` otherwise.
58+
```ts
59+
import { authorizeWebsiteAccess } from '@databuddy/rpc/utils/auth';
60+
61+
export const websitesRouter = createTRPCRouter({
62+
get: protectedProcedure.input(z.string()).query(async ({ ctx, input }) => {
63+
const site = await authorizeWebsiteAccess(ctx, input, 'read');
64+
return site;
65+
})
66+
});
67+
```
68+
69+
### Billing Utilities
70+
- `checkAndTrackWebsiteCreation(customerId: string)``{ allowed: boolean, error?: string }`
71+
- `trackWebsiteUsage(customerId: string, value: number)``{ success: boolean }`
72+
- `getBillingCustomerId(userId: string, organizationId?: string|null): Promise<string>`
73+
74+
```ts
75+
import { getBillingCustomerId, checkAndTrackWebsiteCreation } from '@databuddy/rpc/utils/billing';
76+
77+
const customerId = await getBillingCustomerId(ctx.user.id, ctx.user.organizationId);
78+
const { allowed } = await checkAndTrackWebsiteCreation(customerId);
79+
if (!allowed) throw new TRPCError({ code: 'FORBIDDEN' });
80+
```
81+
82+
### Cache Invalidation Helpers
83+
- `invalidateBasicWebsiteCaches(websiteId, websiteCache)`
84+
- `invalidateWebsiteCaches(websiteId, userId, reason?)`
85+
86+
```ts
87+
import { createDrizzleCache, redis } from '@databuddy/redis';
88+
import { invalidateBasicWebsiteCaches, invalidateWebsiteCaches } from '@databuddy/rpc/utils/cache-invalidation';
89+
90+
const websiteCache = createDrizzleCache({ redis, namespace: 'websites' });
91+
await invalidateBasicWebsiteCaches(websiteId, websiteCache);
92+
await invalidateWebsiteCaches(websiteId, ctx.user.id, 'website updated');
93+
```

0 commit comments

Comments
 (0)