Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .cursor/rules/packages/email.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ await sendEmail({
template: <WelcomeEmail
userName="John Doe"
companyName="Acme Corp"
loginUrl="https://app.captable.com/login"
loginUrl="https://cloud.captable.inc/login"
/>
})
```
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@

👷 **Incorporation** (wip) - Captable, Inc. helps you incorporate your company in minutes, with all the necessary legal documents and filings taken care of.

👷 **Cap table management** (wip) - Captable, Inc. helps you keep track of your companys ownership structure, including who owns what percentage of the company, how much stock/options has been issued, and more.
👷 **Cap table management** (wip) - Captable, Inc. helps you keep track of your company's ownership structure, including who owns what percentage of the company, how much stock/options has been issued, and more.

✅ **Fundraise** - Captable, Inc. can help you raise capital, whether its signing standard or custom SAFE or creating and managing fundraising rounds, tracking investor commitments, and more.

✅ **Investor updates** - Delight your investors and team members by sending them regular updates on your companys progress.
✅ **Investor updates** - Delight your investors and team members by sending them regular updates on your company's progress.

✅ **eSign Documents** - Sign SAFE, NDA, contracts, offere letters or any type of documents with Captable Sign.

Expand All @@ -78,7 +78,9 @@ We have a community of developers, designers, and entrepreneurs who are passiona

- [Next.js](https://nextjs.org)
- [Tailwind](https://tailwindcss.com)
- [Prisma ORM](https://prisma.io)
- [Drizzle ORM](https://orm.drizzle.team)
- [tRPC](https://trpc.io)
- [NextAuth.js](https://next-auth.js.org)

---

Expand Down Expand Up @@ -106,7 +108,7 @@ When contributing to <strong>Captable, Inc.</strong>, whether on GitHub or in ot

- <a href="https://docs.docker.com/get-docker/" target="_blank">Install Docker</a> & <a href="https://docs.docker.com/compose/install/" target="_blank">Docker Compose</a>
- <a href="https://github.com/captableinc/captable/fork" target="_blank">Fork</a> & clone the forked repository
- <a href="https://pnpm.io/installation" target="_blank">Install node and pnpm</a>. (optional)
- <a href="https://bun.sh/docs/installation" target="_blank">Install node and bun</a>. (optional)
- Copy `.env.example` to `.env`

```bash
Expand Down
63 changes: 44 additions & 19 deletions apps/captable/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,34 @@ We have a community of developers, designers, and entrepreneurs who are passiona

- [Next.js](https://nextjs.org)
- [Tailwind](https://tailwindcss.com)
- [Prisma ORM](https://prisma.io)
- [Drizzle ORM](https://orm.drizzle.team)

---
<h3 id="background-jobs">Background Jobs</h3>

Captable uses a custom job queue system for handling background tasks like:
- 📧 Email notifications (welcome, password reset, invites)
- 📄 PDF generation (e-signatures, documents)
- 🔄 Data processing and synchronization

**Development Setup:**
```bash
# Start with job processing (recommended)
bun dx

# Or start jobs separately
bun run jobs:dev
```

**Job Management:**
```bash
bun run jobs # Process pending jobs
bun run test-jobs # Queue sample test jobs
bun run jobs stats # View queue statistics
```

Jobs are automatically processed in production via Cron jobs.


<h3 id="start">Getting started</h3>
When contributing to <strong>Captable, Inc.</strong>, whether on GitHub or in other community spaces:
Expand All @@ -106,7 +131,7 @@ When contributing to <strong>Captable, Inc.</strong>, whether on GitHub or in ot

- <a href="https://docs.docker.com/get-docker/" target="_blank">Install Docker</a> & <a href="https://docs.docker.com/compose/install/" target="_blank">Docker Compose</a>
- <a href="https://github.com/captableinc/captable/fork" target="_blank">Fork</a> & clone the forked repository
- <a href="https://pnpm.io/installation" target="_blank">Install node and pnpm</a>. (optional)
- <a href="https://bun.sh/docs/installation" target="_blank">Install node and bun</a>. (optional)
- Copy `.env.example` to `.env`

```bash
Expand All @@ -117,10 +142,10 @@ When contributing to <strong>Captable, Inc.</strong>, whether on GitHub or in ot

```bash

# With pnpm installed
pnpm dx
# With bun installed
bun dx

# Without pnpm installed
# Without bun installed
docker compose up

```
Expand All @@ -129,8 +154,8 @@ When contributing to <strong>Captable, Inc.</strong>, whether on GitHub or in ot

```bash

docker compose exec app pnpm db:migrate
docker compose exec app pnpm db:seed
docker compose exec app bun db:migrate
docker compose exec app bun db:seed

```

Expand All @@ -143,15 +168,15 @@ When contributing to <strong>Captable, Inc.</strong>, whether on GitHub or in ot
- Emails will be intercepted: [http://localhost:8025](http://localhost:8025)
- SMTP will be on PORT `http://localhost:1025`
- Postgres will be on PORT `http://localhost:5432`
- Prisma studio will be on PORT `http://localhost:5555`
- Database studio will be on PORT `http://localhost:5555`

- Frequently used commands
- `docker compose up` - Start the development environment
- `docker compose down` - Stop the development environment
- `docker compose logs -f` - View logs of the running services
- `docker compose up --build` - Rebuild the docker image
- `docker compose run app pnpm db:migrate` - Run database migrations
- `docker compose run app pnpm db:seed` - Seed the database
- `docker compose run app bun db:migrate` - Run database migrations
- `docker compose run app bun db:seed` - Seed the database

---

Expand Down Expand Up @@ -218,23 +243,23 @@ When contributing to <strong>Captable, Inc.</strong>, whether on GitHub or in ot
- Run the following command to install dependencies

```bash
pnpm install
bun install
```

- Run the following command to migrate and seed the database

```bash
pnpm db:migrate
pnpm db:seed
bun db:migrate
bun db:seed
```

- Run the following command to start the development server

```bash
pnpm dev
bun dev

# On a different terminal, run the following command to start the mail server
pnpm email:dev
bun email:dev
```

- App will be running on [http://localhost:3000](http://localhost:3000)
Expand All @@ -243,10 +268,10 @@ When contributing to <strong>Captable, Inc.</strong>, whether on GitHub or in ot
- Postgres will be on PORT `http://localhost:5432`

- Frequently used commands
- `pnpm dev` - Start the development server
- `pnpm email:dev` - Start the mail server
- `pnpm db:migrate` - Run database migrations
- `pnpm db:seed` - Seed the database
- `bun dev` - Start the development server
- `bun email:dev` - Start the mail server
- `bun db:migrate` - Run database migrations
- `bun db:seed` - Seed the database

<h4 id="changes">Implement your changes</h4>

Expand Down
30 changes: 30 additions & 0 deletions apps/captable/app/api/cron/cleanup-jobs/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { logger } from "@captable/logger";
import { cleanupJobs } from "@captable/queue";
import { type NextRequest, NextResponse } from "next/server";

const log = logger.child({ module: "cron-cleanup" });

export async function GET(request: NextRequest) {
// Verify cron secret
const authHeader = request.headers.get("authorization");
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response("Unauthorized", { status: 401 });
}

try {
const cleaned = await cleanupJobs(7); // Clean jobs older than 7 days

log.info({ cleaned }, "Job cleanup completed");

return NextResponse.json({
success: true,
cleaned,
});
} catch (error) {
log.error({ error }, "Job cleanup failed");
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}
63 changes: 63 additions & 0 deletions apps/captable/app/api/cron/process-jobs/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { logger } from "@captable/logger";
import { processJobs } from "@captable/queue";
import { type NextRequest, NextResponse } from "next/server";
import "@/jobs"; // Import to register all jobs

const log = logger.child({ module: "cron-jobs" });

export async function GET(request: NextRequest) {
// Verify cron secret for security
const authHeader = request.headers.get("authorization");
const expectedAuth = `Bearer ${process.env.CRON_SECRET}`;

if (!expectedAuth || authHeader !== expectedAuth) {
log.warn({ authHeader }, "Unauthorized cron job access attempt");
return new Response("Unauthorized", { status: 401 });
}

try {
const startTime = Date.now();

// Process jobs in batches
let totalProcessed = 0;
let batchCount = 0;
const maxBatches = 10; // Prevent infinite loops

while (batchCount < maxBatches) {
const processed = await processJobs(20); // Process 20 jobs per batch
totalProcessed += processed;
batchCount++;

if (processed === 0) {
break; // No more jobs to process
}

// Small delay between batches
await new Promise((resolve) => setTimeout(resolve, 100));
}

const duration = Date.now() - startTime;

log.info(
{
totalProcessed,
batches: batchCount,
duration,
},
"Cron job processing completed",
);

return NextResponse.json({
success: true,
processed: totalProcessed,
batches: batchCount,
duration,
});
} catch (error) {
log.error({ error }, "Cron job processing failed");
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const ForgotPassword = () => {
},
});
const onSubmit = async (values: z.infer<typeof inputSchema>) => {
await mutateAsync(values.email);
await mutateAsync(values);
};

return (
Expand Down
29 changes: 24 additions & 5 deletions apps/captable/jobs/auth-verification-email.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { env } from "@/env";
import { BaseJob } from "@/jobs/base";
import { sendMail } from "@/server/mailer";
import type { Job } from "pg-boss";
import { logger } from "@captable/logger";
import { BaseJob } from "@captable/queue";

const log = logger.child({ module: "auth-verification-email-job" });

export type AuthVerificationEmailPayloadType = {
email: string;
Expand All @@ -11,6 +12,8 @@ export type AuthVerificationEmailPayloadType = {
const sendAuthVerificationEmail = async (
payload: AuthVerificationEmailPayloadType,
) => {
log.info({ email: payload.email }, "Sending auth verification email");

// Dynamic import to avoid build-time processing
const { render } = await import("@captable/email");
const { AccountVerificationEmail } = await import(
Expand All @@ -28,14 +31,30 @@ const sendAuthVerificationEmail = async (
subject: "Verify your account",
html,
});

log.info(
{ email: payload.email },
"Auth verification email sent successfully",
);
};

export { sendAuthVerificationEmail };

export class AuthVerificationEmailJob extends BaseJob<AuthVerificationEmailPayloadType> {
readonly type = "email.auth-verify";
protected readonly options = {
maxAttempts: 5, // Critical for account verification
retryDelay: 1000,
priority: 3, // High priority
};

async work(job: Job<AuthVerificationEmailPayloadType>): Promise<void> {
await sendAuthVerificationEmail(job.data);
async work(payload: AuthVerificationEmailPayloadType): Promise<void> {
await sendAuthVerificationEmail(payload);
}
}

// Create and register the job instance
const authVerificationEmailJob = new AuthVerificationEmailJob();
authVerificationEmailJob.register();

export { authVerificationEmailJob };
Loading
Loading