First off, thank you for considering contributing to Sendable! 🎉
The following is a set of guidelines for contributing to this project. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
Monorepo note:
sendable-monorepois a Turborepo (pnpm workspace) with two apps —apps/web(Next.js + Convex, implemented) andapps/api(FastAPI, planned/empty scaffold). Almost all contributions today targetapps/web. See the rootREADME.mdfor the full repository layout.
- Code of Conduct
- Getting Started
- Development Workflow
- Project Structure
- Coding Standards
- Commit Guidelines
- Pull Request Process
- Testing
- Documentation
This project and everyone participating in it is governed by our commitment to fostering an open and welcoming environment. Please be respectful and professional in all interactions.
- Be Respectful: Treat everyone with respect and kindness
- Be Collaborative: Work together towards the common goal
- Be Patient: Help others learn and grow
- Accept Feedback: Be open to constructive criticism
- Focus on What's Best: Prioritize the project and community
Before you begin, ensure you have:
- Node.js (v20.x or higher)
- pnpm v10.17.1 (pinned via the
packageManagerfield — use Corepack to match it automatically:corepack enable && corepack prepare pnpm@10.17.1 --activate) - Git
- A Convex account (Sign up free)
- A code editor (VS Code recommended)
-
Fork the Repository
# Click the "Fork" button on GitHub -
Clone Your Fork
git clone https://github.com/hasnaintypes/sendable-monorepo.git cd sendable-monorepo -
Add Upstream Remote
git remote add upstream https://github.com/hasnaintypes/sendable-monorepo.git
-
Install Dependencies (run from the repo root — this installs all workspaces)
pnpm install
-
Set Up Environment Variables
cp apps/web/.env.example apps/web/.env.local # Edit apps/web/.env.local with your credentials -
Start Development Server (run from the repo root)
pnpm dev
The first run triggers Convex's
predevhook (convex dev --skip-push), which walks you through provisioning your Convex deployment. Set the remaining environment variables on the Convex dashboard (Settings → Environment Variables) as well — Convex actions don't read.env.local.
We follow a feature-branch workflow:
main (production)
↓
develop (staging)
↓
feature/your-feature-name (your work)
# Update your local main branch
git checkout main
git pull upstream main
# Create and switch to a new branch
git checkout -b feature/your-feature-name
# Or for bug fixes
git checkout -b fix/bug-description
# Or for documentation
git checkout -b docs/what-you-are-documentingfeature/- New features (e.g.,feature/lead-scoring)fix/- Bug fixes (e.g.,fix/email-validation)docs/- Documentation updates (e.g.,docs/api-reference)refactor/- Code refactoring (e.g.,refactor/auth-flow)test/- Adding tests (e.g.,test/campaign-creation)chore/- Maintenance tasks (e.g.,chore/update-dependencies)
# Fetch latest changes from upstream
git fetch upstream
# Rebase your branch onto upstream/main
git rebase upstream/main
# If there are conflicts, resolve them and continue
git add .
git rebase --continue
# Force push to your fork (only for your feature branches!)
git push origin feature/your-feature-name --forcesendable-monorepo/
├── apps/
│ ├── web/ # Next.js 16 + Convex — the only implemented app
│ │ ├── convex/ # Backend (Convex functions, schema per module)
│ │ ├── src/ # Next.js App Router, components, lib
│ │ └── CONTRIBUTING.md # (this file lives at the repo root now)
│ └── api/ # FastAPI agent backend — planned, empty scaffold
├── docs/ # Canonical architecture & feature reference
└── turbo.json / pnpm-workspace.yaml
For the full, up-to-date directory tree of apps/web (Convex modules, routes, components), see apps/web/README.md rather than duplicating it here — it changes often enough that a second copy would go stale.
apps/web/convex/schema.ts- Database schema (modify carefully; composes per-module tables fromconvex/*/schema.ts)apps/web/src/app/layout.tsx- Root layoutapps/web/src/lib/auth/client.tsx- Client-side authapps/web/convex/auth/helpers.ts- Server-side auth (Better Auth instance + plugin config)
- Always use TypeScript - No plain JavaScript files
- Define types explicitly - Avoid
anyunless absolutely necessary - Use interfaces for objects - Prefer
interfaceovertypefor object shapes - Export types - Make types reusable across files
// Good
interface Lead {
id: string;
email: string;
company: string;
status: "new" | "contacted" | "replied";
}
// Bad
const lead: any = { ... };- Use functional components - No class components
- Use hooks - Leverage React hooks for state and effects
- One component per file - Keep files focused
- Export named components - Avoid default exports for components
// Good
export function LeadCard({ lead }: { lead: Lead }) {
const [isExpanded, setIsExpanded] = useState(false);
return (
<Card>
{/* Component content */}
</Card>
);
}
// Bad
export default ({ lead }) => <div>...</div>- Use clear function names - Describe what the function does
- Validate inputs - Always check arguments
- Use transactions - For multi-document operations
- Handle errors gracefully - Return meaningful error messages
// Good — follows the ownership-check pattern used throughout convex/campaigns/
export const create = mutation({
args: {
name: v.string(),
goal: v.string(),
},
handler: async (ctx, args) => {
const user = await authComponent.safeGetAuthUser(ctx);
if (!user) throw new Error("Not authenticated");
return await ctx.db.insert("campaigns", {
userId: user._id,
...args,
createdAt: Date.now(),
updatedAt: Date.now(),
});
},
});- Use Tailwind CSS - Utility-first approach
- Follow shadcn/ui patterns - For consistent component styling
- Mobile-first - Design for mobile, enhance for desktop
- Dark mode support - Use theme-aware classes
// Good
<div className="flex flex-col gap-4 p-6 md:flex-row md:gap-6 md:p-8">
<button className="bg-primary text-primary-foreground hover:bg-primary/90">
Click me
</button>
</div>
// Bad
<div style={{ display: "flex", padding: "24px" }}>
<button style={{ background: "#007bff" }}>Click me</button>
</div>- React components: PascalCase (e.g.,
LeadCard.tsx) - Utilities/hooks: camelCase (e.g.,
useLeadFilter.ts) - Convex functions: camelCase (e.g.,
mutations.ts,queries.ts) - Pages: lowercase with hyphens (e.g.,
forget-password/page.tsx)
We follow Conventional Commits for clear commit history.
<type>(<scope>): <subject>
<body>
<footer>
feat:- New featurefix:- Bug fixdocs:- Documentation changesstyle:- Code style changes (formatting, no logic change)refactor:- Code refactoringperf:- Performance improvementstest:- Adding or updating testschore:- Maintenance tasksci:- CI/CD changes
# Feature
git commit -m "feat(leads): add CSV import functionality"
# Bug fix
git commit -m "fix(auth): correct redirect after password reset"
# Documentation
git commit -m "docs(readme): update installation instructions"
# Refactor
git commit -m "refactor(campaigns): simplify email generation logic"
# Multiple paragraphs
git commit -m "feat(analytics): add campaign performance dashboard
- Add funnel visualization
- Implement reply rate tracking
- Add sentiment analysis chart
Closes #123"- Atomic commits - Each commit should be a single logical change
- Present tense - "Add feature" not "Added feature"
- Imperative mood - "Fix bug" not "Fixes bug"
- Be descriptive - Explain what and why, not just what
- Reference issues - Use "Closes #123" or "Fixes #456"
- Update your branch with the latest main
- Run all checks locally, from the repo root:
pnpm lint pnpm typecheck pnpm format:check pnpm build
- Test your changes thoroughly (manually — there's no automated suite yet, see Testing)
- Update documentation if needed
-
Push your branch to your fork
git push origin feature/your-feature-name
-
Open a PR on GitHub
- Use a clear, descriptive title
- Reference related issues
- Fill out the PR template completely
-
PR Title Format
feat(leads): add bulk delete functionality fix(auth): resolve email verification bug docs(contributing): improve setup instructions
When you open a PR, include:
## Description
Brief description of what this PR does.
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
## Related Issues
Closes #123
## Changes Made
- Added X functionality
- Fixed Y bug
- Updated Z documentation
## Testing Done
- [ ] Tested locally
- [ ] Added unit tests
- [ ] Tested on multiple browsers
- [ ] Tested mobile responsiveness
## Screenshots (if applicable)
[Add screenshots here]
## Checklist
- [ ] My code follows the project's coding standards
- [ ] I have added tests that prove my fix/feature works
- [ ] I have updated the documentation
- [ ] My changes generate no new warnings
- [ ] I have checked my code and corrected any misspellings- Automated checks must pass (linting, type checking, build)
- At least one approval from a maintainer required
- Address feedback promptly and professionally
- Resolve conflicts if any arise
- Squash commits may be required before merge
-
Delete your branch (both local and remote)
git branch -d feature/your-feature-name git push origin --delete feature/your-feature-name
-
Update your main branch
git checkout main git pull upstream main
There is currently no automated test suite in this repo. pnpm typecheck and pnpm lint (both run from the repo root, enforced in CI) are the primary correctness gates today:
pnpm typecheck # Type-checks apps/web's src/ and convex/ separately
pnpm lint # ESLint over src/ and convex/
pnpm format:check # Prettier checkIf you're adding a test runner or writing the first tests for a feature, open an issue first to agree on the approach (framework, scope) before sinking time into it — this is a meaningful, repo-wide decision, not a per-PR one.
- Add JSDoc comments for complex functions
- Explain "why" not "what" - Code shows what, comments explain why
- Keep comments updated - Outdated comments are worse than none
/**
* Calculates lead score based on engagement metrics
*
* Uses a weighted algorithm that prioritizes recent activity:
* - Email opens: 1 point
* - Email clicks: 3 points
* - Replies: 5 points
* - Positive sentiment: 2x multiplier
*
* @param lead - The lead to score
* @returns Score between 0-100
*/
export function calculateLeadScore(lead: Lead): number {
// Implementation
}When updating docs:
- Keep README.md current - Reflect actual functionality
- Update CHANGELOG.md - Document notable changes
- Add inline examples - Show usage in comments
- Create guides - For complex features
If you have questions:
- Check existing Issues
- Search Discussions
- Open a new issue with the
questionlabel
Your contributions make Sendable better for everyone. We appreciate your time and effort!
Happy Coding!