@devwithbobby/loops 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,42 +1,49 @@
1
- # 🔨 Convex Component Template
1
+ # @devwithbobby/loops
2
2
 
3
- [![pkg.pr.new](https://pkg.pr.new/badge/robertalv/loops-component)](https://pkg.pr.new/~/robertalv/loops-component)
3
+ [![npm version](https://img.shields.io/npm/v/@devwithbobby/loops.svg)](https://www.npmjs.com/package/@devwithbobby/loops)
4
4
 
5
- A modern template for building reusable [Convex components](https://www.convex.dev/components) with Bun, TypeScript, and comprehensive testing.
5
+ A Convex component for integrating with [Loops.so](https://loops.so) email marketing platform. Send transactional emails, manage contacts, trigger campaigns and loops, and monitor email operations with built-in spam detection and rate limiting.
6
6
 
7
- > **Note:** Replace `robertalv/loops-component` in the badge above with your GitHub username/organization and repository name once you set up your repository.
7
+ ## Features
8
8
 
9
- ## Getting Started
9
+ - **Contact Management** - Create, update, find, and delete contacts
10
+ - ✅ **Transactional Emails** - Send one-off emails with templates
11
+ - ✅ **Events** - Trigger email workflows based on events
12
+ - ✅ **Campaigns** - Send campaigns to audiences or specific contacts
13
+ - ✅ **Loops** - Trigger automated email sequences
14
+ - ✅ **Monitoring** - Track all email operations with spam detection
15
+ - ✅ **Rate Limiting** - Built-in rate limiting queries for abuse prevention
16
+ - ✅ **Type-Safe** - Full TypeScript support with Zod validation
10
17
 
11
- ### 1. Rename Your Component
12
-
13
- After cloning this template, run the rename script to customize it for your component:
18
+ ## Installation
14
19
 
15
20
  ```bash
16
- bun rename.ts
21
+ npm install @devwithbobby/loops
22
+ # or
23
+ bun add @devwithbobby/loops
17
24
  ```
18
25
 
19
- The script will:
20
- - Prompt you for your component name (e.g., "document search", "rate limiter")
21
- - Ask for your NPM package name (default: `@samhoque/your-component-name`)
22
- - Ask for your GitHub repository (default: `samhoque/your-component-name`)
23
- - Automatically generate all case variants (PascalCase, camelCase, kebab-case, etc.)
24
- - Replace all template placeholders across the entire codebase
25
- - Update `package.json` with your package name
26
- - Optionally delete itself when done
27
-
28
- **What gets renamed:**
29
- - Package name in `package.json`
30
- - All imports and references throughout the codebase
31
- - Component class names, function names, and identifiers
32
- - Documentation examples in README and comments
26
+ ## Quick Start
27
+
28
+ ### 1. Install and Mount the Component
29
+
30
+ In your `convex/convex.config.ts`:
31
+
32
+ ```typescript
33
+ import loops from "@devwithbobby/loops/convex.config";
34
+ import { defineApp } from "convex/server";
35
+
36
+ const app = defineApp();
37
+ app.use(loops);
38
+
39
+ export default app;
40
+ ```
33
41
 
34
42
  ### 2. Set Up Environment Variables
35
43
 
36
44
  **⚠️ IMPORTANT: Set your Loops API key before using the component.**
37
45
 
38
46
  ```bash
39
- # Set the API key in your Convex environment variables
40
47
  npx convex env set LOOPS_API_KEY "your-loops-api-key-here"
41
48
  ```
42
49
 
@@ -44,474 +51,435 @@ npx convex env set LOOPS_API_KEY "your-loops-api-key-here"
44
51
  1. Go to Settings → Environment Variables
45
52
  2. Add `LOOPS_API_KEY` with your Loops.so API key
46
53
 
47
- 📖 **See [ENV_SETUP.md](./ENV_SETUP.md) for detailed setup instructions and security best practices.**
48
-
49
- ### 3. Install Dependencies & Start Development
50
-
51
- ```bash
52
- bun install
53
- cd example && bun install && cd ..
54
- bun run dev:backend
55
- ```
56
-
57
- Then in another terminal:
58
- ```bash
59
- cd example
60
- bun run dev
61
- ```
54
+ Get your API key from [Loops.so Dashboard](https://app.loops.so/settings/api).
62
55
 
63
- ## What are Convex Components?
56
+ ### 3. Use the Component
64
57
 
65
- Components are isolated, reusable units of Convex functionality with their own:
66
- - Functions (queries, mutations, actions)
67
- - Tables and schemas
68
- - File storage
69
- - Scheduled functions
58
+ In your `convex/functions.ts` (or any convex file):
70
59
 
71
- Components plug into Convex apps (or parent components) through a public interface, enabling modular architecture with proper isolation and security.
60
+ ```typescript
61
+ import { Loops } from "@devwithbobby/loops";
62
+ import { components } from "./_generated/api";
63
+ import { action } from "./_generated/server";
64
+ import { v } from "convex/values";
65
+
66
+ // Initialize the Loops client
67
+ const loops = new Loops(components.loops);
68
+
69
+ // Export functions wrapped with auth (required in production)
70
+ export const addContact = action({
71
+ args: {
72
+ email: v.string(),
73
+ firstName: v.optional(v.string()),
74
+ lastName: v.optional(v.string()),
75
+ },
76
+ handler: async (ctx, args) => {
77
+ // Add authentication check
78
+ const identity = await ctx.auth.getUserIdentity();
79
+ if (!identity) throw new Error("Unauthorized");
72
80
 
73
- ## Project Structure
81
+ return await loops.addContact(ctx, args);
82
+ },
83
+ });
74
84
 
75
- ```
76
- src/
77
- component/ # The Convex component source code
78
- convex.config.ts # Component configuration (exported via package.json)
79
- schema.ts # Convex schema definition
80
- lib.ts # Component functions (queries, mutations)
81
- _generated/ # Auto-generated Convex types (gitignored)
82
-
83
- client/ # Optional: Client library that runs in the app
84
- index.ts # Helper class for easier component interaction
85
-
86
- react/ # Optional: React components for UI
87
- index.tsx # React hooks and components
88
-
89
- test/
90
- component/ # Component tests (separate from source)
91
- setup.test.ts # Test setup with module auto-discovery
92
- *.test.ts # Unit tests for the component
93
-
94
- example/
95
- convex/ # Example app that uses the component
96
- convex.config.ts # Example app configuration
97
- schema.ts # Example app schema
98
- _generated/ # Auto-generated types (gitignored)
99
- src/ # Example app frontend
85
+ export const sendWelcomeEmail = action({
86
+ args: {
87
+ email: v.string(),
88
+ name: v.string(),
89
+ },
90
+ handler: async (ctx, args) => {
91
+ const identity = await ctx.auth.getUserIdentity();
92
+ if (!identity) throw new Error("Unauthorized");
93
+
94
+ // Send transactional email
95
+ return await loops.sendTransactional(ctx, {
96
+ transactionalId: "welcome-email-template-id",
97
+ email: args.email,
98
+ dataVariables: {
99
+ name: args.name,
100
+ },
101
+ });
102
+ },
103
+ });
100
104
  ```
101
105
 
102
- ## Key Commands
106
+ ## API Reference
103
107
 
104
- ### Development
105
- ```bash
106
- bun run dev:backend # Start Convex dev with live component sources
107
- bun run build # Build the component for distribution
108
- ```
108
+ ### Contact Management
109
109
 
110
- ### Testing
111
- ```bash
112
- bun test # Run all tests
113
- bun test --watch # Watch mode
114
- bun test --coverage # Generate coverage reports
115
- bun test -t "pattern" # Filter tests by name
116
- CLAUDECODE=1 bun test # AI-friendly quiet output
117
- ```
110
+ #### Add or Update Contact
118
111
 
119
- ### Linting and Formatting
120
- ```bash
121
- bun run lint # Lint with Biome
122
- bun run lint:fix # Auto-fix linting issues
123
- bun run format # Format code with Biome
124
- bun run check # Run both lint and format checks
125
- bun run check:fix # Auto-fix all issues
112
+ ```typescript
113
+ await loops.addContact(ctx, {
114
+ email: "user@example.com",
115
+ firstName: "John",
116
+ lastName: "Doe",
117
+ userId: "user123",
118
+ source: "webapp",
119
+ subscribed: true,
120
+ userGroup: "premium",
121
+ });
126
122
  ```
127
123
 
128
- ### Git Hooks (Lefthook)
129
-
130
- This project uses [Lefthook](https://github.com/evilmartians/lefthook) for Git hooks. Hooks are automatically installed when you run `bun install`.
124
+ #### Update Contact
131
125
 
132
- **Pre-commit hook:**
133
- - Runs Biome check on staged files
134
- - Auto-fixes issues and stages the changes
135
- - Prevents commits with linting/formatting errors
136
-
137
- To skip hooks (not recommended):
138
- ```bash
139
- git commit --no-verify
126
+ ```typescript
127
+ await loops.updateContact(ctx, {
128
+ email: "user@example.com",
129
+ firstName: "Jane",
130
+ userGroup: "vip",
131
+ });
140
132
  ```
141
133
 
142
- ### CI/CD
134
+ #### Find Contact
143
135
 
144
- The project includes a GitHub Actions workflow that runs on every push and pull request:
145
-
146
- **Workflow: Test and lint** (`.github/workflows/test-and-lint.yml`)
147
- - Installs dependencies with Bun
148
- - Builds the project
149
- - Publishes preview packages with `pkg.pr.new`
150
- - Runs all tests
151
- - Runs linting checks
152
-
153
- The workflow ensures code quality and prevents broken builds from being merged.
154
-
155
- #### pkg.pr.new Setup
156
-
157
- This project uses [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new) for continuous package previews. Each commit and PR automatically generates a preview release that can be installed without publishing to npm.
158
-
159
- **One-time setup required:**
160
- 1. Install the [pkg.pr.new GitHub App](https://github.com/apps/pkg-pr-new) on your repository
161
- 2. Once installed, the workflow will automatically publish preview packages on every commit/PR
136
+ ```typescript
137
+ const contact = await loops.findContact(ctx, {
138
+ email: "user@example.com",
139
+ });
140
+ ```
162
141
 
163
- **Using preview packages:**
164
- ```bash
165
- # Install from a specific commit (Bun)
166
- bun add https://pkg.pr.new/robertalv/loops-component/robertalv/loops-component@COMMIT_SHA
142
+ #### Delete Contact
167
143
 
168
- # Or with npm
169
- npm i https://pkg.pr.new/robertalv/loops-component/robertalv/loops-component@COMMIT_SHA
144
+ ```typescript
145
+ await loops.deleteContact(ctx, {
146
+ email: "user@example.com",
147
+ });
170
148
  ```
171
149
 
172
- Preview URLs will be posted as comments on your pull requests automatically.
173
-
174
- ## Component Architecture
150
+ #### Batch Create Contacts
175
151
 
176
- ### 1. Component Definition
152
+ ```typescript
153
+ await loops.batchCreateContacts(ctx, {
154
+ contacts: [
155
+ { email: "user1@example.com", firstName: "John" },
156
+ { email: "user2@example.com", firstName: "Jane" },
157
+ ],
158
+ });
159
+ ```
177
160
 
178
- The component is defined in `src/component/convex.config.ts`:
161
+ #### Unsubscribe/Resubscribe
179
162
 
180
163
  ```typescript
181
- import { defineComponent } from "convex/server";
182
- import { api } from "./_generated/api";
183
-
184
- const component = defineComponent("loopsComponent"); // Change "loopsComponent" to your component name
185
- component.export(api, { greet: api.lib.greet });
186
- export default component;
164
+ await loops.unsubscribeContact(ctx, { email: "user@example.com" });
165
+ await loops.resubscribeContact(ctx, { email: "user@example.com" });
187
166
  ```
188
167
 
189
- ### 2. Package Exports
168
+ #### Count Contacts
190
169
 
191
- The `package.json` exports the component using the `@convex-dev/component-source` condition:
170
+ ```typescript
171
+ // Count all contacts
172
+ const total = await loops.countContacts(ctx, {});
192
173
 
193
- ```json
194
- {
195
- "exports": {
196
- "./convex.config": {
197
- "@convex-dev/component-source": "./src/component/convex.config.ts"
198
- }
199
- }
200
- }
174
+ // Count by filter
175
+ const premium = await loops.countContacts(ctx, {
176
+ userGroup: "premium",
177
+ subscribed: true,
178
+ });
201
179
  ```
202
180
 
203
- This enables live reloading during development.
181
+ ### Email Sending
204
182
 
205
- ### 3. Component Usage
206
-
207
- Apps import and mount the component in their `convex.config.ts`:
183
+ #### Send Transactional Email
208
184
 
209
185
  ```typescript
210
- import { defineApp } from "convex/server";
211
- import component from "@your-package/convex.config";
212
-
213
- const app = defineApp();
214
- app.use(component);
215
- export default app;
186
+ await loops.sendTransactional(ctx, {
187
+ transactionalId: "template-id-from-loops",
188
+ email: "user@example.com",
189
+ dataVariables: {
190
+ name: "John",
191
+ orderId: "12345",
192
+ },
193
+ });
216
194
  ```
217
195
 
218
- ### 4. Client Library (Optional)
219
-
220
- The `src/client/` directory contains helper code that runs in the app (not the component):
196
+ #### Send Event (Triggers Workflows)
221
197
 
222
198
  ```typescript
223
- export class Counter {
224
- constructor(
225
- private component: UseApi<typeof api>,
226
- private options?: { initialValue?: number }
227
- ) {}
228
-
229
- async count(ctx: RunQueryCtx) {
230
- return await ctx.runQuery(this.component.public.count, {});
231
- }
232
- }
199
+ await loops.sendEvent(ctx, {
200
+ email: "user@example.com",
201
+ eventName: "purchase_completed",
202
+ eventProperties: {
203
+ product: "Premium Plan",
204
+ amount: 99.99,
205
+ },
206
+ });
233
207
  ```
234
208
 
235
- This pattern is useful for:
236
- - Hiding implementation details
237
- - Managing implicit dependencies (auth, env vars)
238
- - Providing a cleaner API surface
209
+ #### Send Campaign
239
210
 
240
- ## Calling Component Functions
211
+ ```typescript
212
+ // Send to specific emails
213
+ await loops.sendCampaign(ctx, {
214
+ campaignId: "campaign-id-from-loops",
215
+ emails: ["user1@example.com", "user2@example.com"],
216
+ dataVariables: { discount: "20%" },
217
+ });
241
218
 
242
- ### Subtransactions
219
+ // Send to audience
220
+ await loops.sendCampaign(ctx, {
221
+ campaignId: "campaign-id-from-loops",
222
+ audienceFilters: {
223
+ userGroup: "premium",
224
+ source: "webapp",
225
+ },
226
+ });
227
+ ```
243
228
 
244
- Components use **subtransactions** for cross-component function calls:
229
+ #### Trigger Loop (Automated Sequence)
245
230
 
246
231
  ```typescript
247
- // From the app or parent component
248
- const count = await ctx.runQuery(components.counter.public.count, args);
249
- const newCount = await ctx.runMutation(components.counter.public.increment, args);
232
+ await loops.triggerLoop(ctx, {
233
+ loopId: "loop-id-from-loops",
234
+ email: "user@example.com",
235
+ dataVariables: {
236
+ onboardingStep: "welcome",
237
+ },
238
+ });
250
239
  ```
251
240
 
252
- **Key semantics:**
253
- 1. Sub-queries track reads for reactivity across components
254
- 2. Sub-mutations contribute to the parent's ACID transaction
255
- 3. Sub-mutations are isolated from each other (even with `Promise.all`)
256
- 4. Parent errors roll back sub-mutations
257
- 5. Sub-mutation errors can be caught without affecting parent
241
+ ### Monitoring & Analytics
258
242
 
259
- ### Exposing Component Functions Publicly
260
-
261
- Components cannot be called directly from clients. The app must wrap them:
243
+ #### Get Email Statistics
262
244
 
263
245
  ```typescript
264
- // in the app's convex/counter.ts
265
- export const count = query({
266
- handler: async (ctx) => {
267
- return await ctx.runQuery(components.counter.public.count, {});
268
- },
246
+ const stats = await loops.getEmailStats(ctx, {
247
+ timeWindowMs: 3600000, // Last hour
269
248
  });
249
+
250
+ console.log(stats.totalOperations); // Total emails sent
251
+ console.log(stats.successfulOperations); // Successful sends
252
+ console.log(stats.failedOperations); // Failed sends
253
+ console.log(stats.operationsByType); // Breakdown by type
254
+ console.log(stats.uniqueRecipients); // Unique email addresses
270
255
  ```
271
256
 
272
- This allows the app to add auth, rate limiting, etc.
257
+ #### Detect Spam Patterns
273
258
 
274
- ## Working with Isolation
259
+ ```typescript
260
+ // Detect recipients with suspicious activity
261
+ const spamRecipients = await loops.detectRecipientSpam(ctx, {
262
+ timeWindowMs: 3600000,
263
+ maxEmailsPerRecipient: 10,
264
+ });
275
265
 
276
- ### Function Access Hierarchy
266
+ // Detect actors with suspicious activity
267
+ const spamActors = await loops.detectActorSpam(ctx, {
268
+ timeWindowMs: 3600000,
269
+ maxEmailsPerActor: 50,
270
+ });
277
271
 
278
- ```mermaid
279
- flowchart TD
280
- A[Public Internet / React] --> B[App]
281
- B --> C[Component1]
282
- B --> D[Component2]
283
- C --> E[Component3]
272
+ // Detect rapid-fire patterns
273
+ const rapidFire = await loops.detectRapidFirePatterns(ctx, {
274
+ timeWindowMs: 60000, // Last minute
275
+ maxEmailsPerWindow: 5,
276
+ });
284
277
  ```
285
278
 
286
- - Clients can only call app functions (not component functions)
287
- - Apps can call their own functions and component public functions
288
- - Components can only call their own functions and child component public functions
289
-
290
- ### Function Handles
279
+ ### Rate Limiting
291
280
 
292
- To allow components to call back into the app, use function handles:
281
+ #### Check Rate Limits
293
282
 
294
283
  ```typescript
295
- // In the app
296
- const handle = await createFunctionHandle(api.myMutation);
284
+ // Check recipient rate limit
285
+ const recipientCheck = await loops.checkRecipientRateLimit(ctx, {
286
+ email: "user@example.com",
287
+ timeWindowMs: 3600000, // 1 hour
288
+ maxEmails: 10,
289
+ });
297
290
 
298
- // Pass handle to component
299
- await ctx.runMutation(components.worker.public.process, { handler: handle });
291
+ if (!recipientCheck.allowed) {
292
+ throw new Error(`Rate limit exceeded. Try again after ${recipientCheck.retryAfter}ms`);
293
+ }
300
294
 
301
- // In the component
302
- export const process = mutation({
303
- args: { handler: v.string() },
304
- handler: async (ctx, args) => {
305
- // Component can now call the app's function
306
- const functionHandle: FunctionHandle<"mutation"> = args.handler;
307
- await ctx.runMutation(functionHandle, {});
308
- },
295
+ // Check actor rate limit
296
+ const actorCheck = await loops.checkActorRateLimit(ctx, {
297
+ actorId: "user123",
298
+ timeWindowMs: 60000, // 1 minute
299
+ maxEmails: 20,
300
+ });
301
+
302
+ // Check global rate limit
303
+ const globalCheck = await loops.checkGlobalRateLimit(ctx, {
304
+ timeWindowMs: 60000,
305
+ maxEmails: 1000,
309
306
  });
310
307
  ```
311
308
 
312
- **Use cases:**
313
- - Migrations component iterating over app tables
314
- - Webhook handlers calling app logic
315
- - Background job processors
309
+ **Example: Rate-limited email sending**
316
310
 
317
- ### Table Access
311
+ ```typescript
312
+ export const sendTransactionalWithRateLimit = action({
313
+ args: {
314
+ transactionalId: v.string(),
315
+ email: v.string(),
316
+ actorId: v.optional(v.string()),
317
+ },
318
+ handler: async (ctx, args) => {
319
+ const identity = await ctx.auth.getUserIdentity();
320
+ if (!identity) throw new Error("Unauthorized");
318
321
 
319
- Components have **isolated tables**:
320
- - Components can only read/write their own tables
321
- - Use `v.id("tableName")` for component tables
322
- - Use `v.string()` for IDs from other components/app
323
- - Use function handles to grant table access across boundaries
322
+ const actorId = args.actorId ?? identity.subject;
324
323
 
325
- ### Environment Variables and Auth
324
+ // Check rate limit before sending
325
+ const rateLimitCheck = await loops.checkActorRateLimit(ctx, {
326
+ actorId,
327
+ timeWindowMs: 60000, // 1 minute
328
+ maxEmails: 10,
329
+ });
326
330
 
327
- Components **cannot access** `process.env` or `ctx.auth` directly. Pass them through:
331
+ if (!rateLimitCheck.allowed) {
332
+ throw new Error(
333
+ `Rate limit exceeded. Please try again after ${rateLimitCheck.retryAfter}ms.`
334
+ );
335
+ }
328
336
 
329
- ```typescript
330
- // src/client/index.ts (runs in app context)
331
- class MyComponent {
332
- constructor(
333
- private component: UseApi<typeof api>,
334
- private options?: { apiKey?: string }
335
- ) {
336
- this.apiKey = options?.apiKey ?? process.env.MY_API_KEY;
337
- }
338
-
339
- async doSomething(ctx: QueryCtx) {
340
- return await ctx.runQuery(this.component.public.process, {
341
- apiKey: this.apiKey,
342
- auth: await ctx.auth.getUserIdentity(),
337
+ // Send email
338
+ return await loops.sendTransactional(ctx, {
339
+ ...args,
340
+ actorId,
343
341
  });
344
- }
345
- }
342
+ },
343
+ });
346
344
  ```
347
345
 
348
- ### HTTP Actions
346
+ ## Using the API Helper
349
347
 
350
- Components cannot define HTTP routes directly. Instead, they export handlers that the app mounts:
348
+ The component also exports an `api()` helper for easier re-exporting:
351
349
 
352
350
  ```typescript
353
- // src/client/index.ts
354
- export const httpHandler = httpAction(async (ctx, request) => {
355
- // Handle HTTP request
356
- });
357
-
358
- // In app's convex/http.ts
359
- import { httpRouter } from "convex/server";
360
- import { httpHandler } from "@your-component/client";
361
-
362
- const http = httpRouter();
363
- http.route({ path: "/webhook", method: "POST", handler: httpHandler });
364
- export default http;
351
+ import { Loops } from "@devwithbobby/loops";
352
+ import { components } from "./_generated/api";
353
+
354
+ const loops = new Loops(components.loops);
355
+
356
+ // Export all functions at once
357
+ export const {
358
+ addContact,
359
+ updateContact,
360
+ sendTransactional,
361
+ sendEvent,
362
+ sendCampaign,
363
+ triggerLoop,
364
+ countContacts,
365
+ // ... all other functions
366
+ } = loops.api();
365
367
  ```
366
368
 
367
- ### Pagination
368
-
369
- The built-in `.paginate()` doesn't work in components. Use [`convex-helpers` paginator](https://github.com/get-convex/convex-helpers) instead:
369
+ **⚠️ Security Warning:** The `api()` helper exports functions without authentication. Always wrap these functions with auth checks in production:
370
370
 
371
371
  ```typescript
372
- import { paginationOptsValidator } from "convex-helpers/server/pagination";
373
- import { makePagination } from "convex-helpers/server/pagination";
374
-
375
- export const listItems = query({
376
- args: { paginationOpts: paginationOptsValidator },
372
+ export const addContact = action({
373
+ args: { email: v.string(), ... },
377
374
  handler: async (ctx, args) => {
378
- return await makePagination(ctx.db.query("items"), args.paginationOpts);
375
+ const identity = await ctx.auth.getUserIdentity();
376
+ if (!identity) throw new Error("Unauthorized");
377
+
378
+ return await loops.addContact(ctx, args);
379
379
  },
380
380
  });
381
381
  ```
382
382
 
383
- ## Testing Pattern
383
+ ## Security Best Practices
384
384
 
385
- This template uses `convex-test` with Bun's test runner. Tests are **separate from component source** to prevent bundling issues.
386
-
387
- ### Test Setup (`test/component/setup.test.ts`)
388
-
389
- Auto-discovers component files and creates a test helper:
390
-
391
- ```typescript
392
- import { convexTest as baseConvexTest } from "convex-test";
393
- import { Glob } from "bun";
394
-
395
- const glob = new Glob("**/*.ts");
396
- const modules: Record<string, string> = {};
397
-
398
- for await (const file of glob.scan("./src/component")) {
399
- if (!file.startsWith("_generated/")) {
400
- modules[file.replace(/\.ts$/, ".js")] = await Bun.file(
401
- `./src/component/${file}`
402
- ).text();
403
- }
404
- }
385
+ 1. **Always add authentication** - Wrap all functions with auth checks
386
+ 2. **Use environment variables** - Store API key in Convex environment variables
387
+ 3. **Implement rate limiting** - Use the built-in rate limiting queries
388
+ 4. **Monitor for abuse** - Use spam detection queries to identify suspicious patterns
389
+ 5. **Sanitize errors** - Don't expose sensitive error details to clients
405
390
 
406
- export const convexTest = () => baseConvexTest(schema, modules);
407
- ```
391
+ See [SECURITY.md](./prds/SECURITY.md) for detailed security guidelines.
408
392
 
409
- ### Writing Tests
393
+ ## Monitoring & Rate Limiting
410
394
 
411
- ```typescript
412
- import { test, expect } from "bun:test";
413
- import { api } from "../../src/component/_generated/api";
414
- import { convexTest } from "./setup.test";
415
-
416
- test("greet returns greeting", async () => {
417
- const t = convexTest();
418
- const result = await t.query(api.lib.greet, { name: "Alice" });
419
- expect(result).toBe("Hello, Alice!");
420
- });
395
+ The component automatically logs all email operations for monitoring. Use the built-in queries to:
421
396
 
422
- test("with authentication", async () => {
423
- const t = convexTest();
424
- const asUser = t.withIdentity({ subject: "user123" });
425
- const result = await asUser.query(api.lib.getCurrentUser, {});
426
- expect(result.subject).toBe("user123");
427
- });
428
- ```
397
+ - Track email statistics
398
+ - Detect spam patterns
399
+ - Enforce rate limits
400
+ - Monitor for abuse
429
401
 
430
- ## Distribution
402
+ See [MONITORING.md](./prds/MONITORING.md) and [RATE_LIMITING.md](./prds/RATE_LIMITING.md) for detailed guides.
431
403
 
432
- ### Local Development
404
+ ## Environment Variables
433
405
 
434
- To use the component in another app during development:
406
+ Set `LOOPS_API_KEY` in your Convex environment:
435
407
 
436
408
  ```bash
437
- bun run build
438
- bun pack # or npm pack
409
+ npx convex env set LOOPS_API_KEY "your-api-key"
439
410
  ```
440
411
 
441
- Then in the other app:
442
- ```bash
443
- bun install ../path/to/component/your-component-0.1.0.tgz
444
- ```
412
+ See [ENV_SETUP.md](./prds/ENV_SETUP.md) for detailed setup instructions.
445
413
 
446
- ### Publishing
414
+ ## Development
447
415
 
448
- This package is currently marked as `private` in package.json. To publish to npm:
416
+ ### Local Development
449
417
 
450
- 1. Remove `"private": true` from package.json
451
- 2. Run: `bun publish` (or `npm publish` if bun publish is not yet available)
418
+ To use this component in development with live reloading:
452
419
 
453
- ## Dashboard and Deployment
420
+ ```bash
421
+ bun run dev:backend
422
+ ```
454
423
 
455
- ### Component Visibility
424
+ This starts Convex dev with `--live-component-sources` enabled, allowing changes to be reflected immediately.
456
425
 
457
- In the Convex dashboard, you can select each component to see:
458
- - Data tables
459
- - Functions
460
- - File storage
461
- - Logs
462
- - Scheduled functions
426
+ ### Building
463
427
 
464
- ### Deployment Semantics
428
+ ```bash
429
+ npm run build
430
+ ```
465
431
 
466
- 1. **Function calls**: Top-level query/mutation counts as single function call (sub-calls are free)
467
- 2. **Database bandwidth**: Component functions count bandwidth separately
468
- 3. **Logging**: Component logs appear in dashboard and log streams
469
- 4. **Exports**: Snapshot exports include all component data
470
- 5. **Streaming exports**: Only include top-level app data (not components)
432
+ ### Testing
471
433
 
472
- ## Code Style
434
+ ```bash
435
+ npm test
436
+ ```
473
437
 
474
- - **Package manager**: Bun
475
- - **Linter/Formatter**: Biome
476
- - **Indentation**: Tabs
477
- - **Quotes**: Double quotes
478
- - **TypeScript**: Strict mode with extra checks
438
+ ## Project Structure
479
439
 
480
- ## Examples
440
+ ```
441
+ src/
442
+ component/ # The Convex component
443
+ convex.config.ts # Component configuration
444
+ schema.ts # Database schema
445
+ lib.ts # Component functions
446
+ validators.ts # Zod validators
447
+ tables/ # Table definitions
448
+
449
+ client/ # Client library
450
+ index.ts # Loops client class
451
+ types.ts # TypeScript types
452
+
453
+ example/ # Example app
454
+ convex/
455
+ example.ts # Example usage
456
+ ```
481
457
 
482
- ### First-Party Components
458
+ ## API Coverage
483
459
 
484
- All first-party components are open source:
460
+ This component implements the following Loops.so API endpoints:
485
461
 
486
- - [loops-component](https://github.com/get-convex/loops-component) - Attaching components to app tables with triggers
487
- - [twilio](https://github.com/get-convex/twilio) - HTTP actions and webhooks
488
- - [aggregate](https://github.com/get-convex/aggregate) - Testing patterns
489
- - [migrations](https://github.com/get-convex/migrations) - Function handles and table access
462
+ - ✅ Create/Update Contact
463
+ - Delete Contact
464
+ - Find Contact
465
+ - Batch Create Contacts
466
+ - ✅ Unsubscribe/Resubscribe Contact
467
+ - ✅ Count Contacts (custom implementation)
468
+ - ✅ Send Transactional Email
469
+ - ✅ Send Event
470
+ - ✅ Send Campaign
471
+ - ✅ Trigger Loop
490
472
 
491
- ## Important Notes
473
+ ## Contributing
492
474
 
493
- - **Generated files**: Never edit `_generated/` directories
494
- - **Test location**: Always place tests in `test/component/` (not `src/component/`)
495
- - **Component name**: Change `"loopsComponent"` in convex.config.ts to your component name
496
- - **Live reloading**: Enabled via `--live-component-sources` flag
497
- - **Peer dependencies**: Component uses the app's Convex installation
475
+ Contributions are welcome! Please open an issue or submit a pull request.
498
476
 
499
- ## Documentation
477
+ ## License
500
478
 
501
- - **[ENV_SETUP.md](./ENV_SETUP.md)** - Environment variable setup and security best practices
502
- - **[SECURITY.md](./SECURITY.md)** - Security considerations and guidelines
503
- - **[MONITORING.md](./MONITORING.md)** - Email monitoring and spam detection
504
- - **[RATE_LIMITING.md](./RATE_LIMITING.md)** - Rate limiting implementation guide
479
+ Apache-2.0
505
480
 
506
481
  ## Resources
507
482
 
483
+ - [Loops.so Documentation](https://loops.so/docs)
508
484
  - [Convex Components Documentation](https://www.convex.dev/components)
509
- - [Component Authoring Guide](https://docs.convex.dev/components/authoring)
510
485
  - [Convex Environment Variables](https://docs.convex.dev/production/environment-variables)
511
- - [convex-test](https://github.com/get-convex/convex-test)
512
- - [convex-helpers](https://github.com/get-convex/convex-helpers)
513
- - [Bun Documentation](https://bun.sh/docs)
514
-
515
- ## License
516
-
517
- MIT