Biz Dev

Pipeline Outreach
Biz Dev · contacts, pipeline and outreach

Sign in

Enter your email and we'll send a link to set a new password.

Choose a new password, at least 12 characters.

Home

Follow-ups due in the next 7 days

Pipeline

Categories

Select a contact, or create one.

New inbound prospect

Recipients

Message

Merge fields. Click one to insert it where the cursor is:

Each recipient gets their own email. Contacts without an email address are skipped.

Social

Pretend task completed. For this to really be done, you'll need the LinkedIn API and the Meta (Facebook and Instagram) Graph API.

Help: how this build works

Biz Dev is the business-development console for the firm: contacts, the inbound prospect pipeline, outreach email, and external profiles. Sign in at crm-9yu.pages.dev. Accounts are email and password, with a Cloudflare Turnstile check and a reset link by email.

Claimed: 75% better than every other CRM combined. This is a joke, and the figure has not been measured.

Code

  • GitHub repository: TeoDestreo/CRM. Production branch: main. Working branch: claude/biz-dev-github-cloudflare-crm-bpm9r7.
  • index.html: the whole front end. Plain HTML, CSS and JavaScript, with no build step. Uses the Quill 1.3.7 editor from cdnjs.
  • functions/api/[[route]].js: the API, a Cloudflare Pages Function serving /api/*.
  • db/schema.sql: the database schema. Applied to D1 with the Cloudflare D1 query API.
  • CLAUDE.md: project notes for future work sessions.

Hosting

  • Cloudflare Pages project crm, in the account "Advisors@fogler.pro's Account" (account ID ef7d930dd08e89e749df7b27c60f0684).
  • Production branch main deploys to crm-9yu.pages.dev. Other branches get preview deployments.
  • Pushes to a branch deploy automatically. Nothing reaches production until it is pushed to main.

Databases

  • biz-dev-crm (Cloudflare D1, SQLite). Database ID d9052535-f38d-4c3e-b2d4-67967f5ae078. Bound to the Pages project as DB for production and preview.
  • Other databases in the same account, not used by this app: advancedcpe-lms and advancedcpe-lms-test.

Data dictionary

Generated from db/schema.sql. Column types and constraints are shown exactly as defined there.

contacts

People and organizations the firm does business with or may do business with.

ColumnTypeConstraints and defaults
idINTEGERPRIMARY KEY AUTOINCREMENT
first_nameTEXT 
last_nameTEXT 
companyTEXT 
titleTEXT 
emailTEXT 
phoneTEXT 
addressTEXT 
websiteTEXT 
linkedin_urlTEXT 
facebook_urlTEXT 
instagram_urlTEXT 
notesTEXT 
relationshipTEXTNOT NULL DEFAULT 'future' CHECK (relationship IN ('customer', 'future'))
google_contact_idTEXT 
created_atTEXTNOT NULL DEFAULT (datetime('now'))
updated_atTEXTNOT NULL DEFAULT (datetime('now'))

categories

User-defined labels for contacts, such as Referral Partner or Professional Contact.

ColumnTypeConstraints and defaults
idINTEGERPRIMARY KEY AUTOINCREMENT
nameTEXTNOT NULL UNIQUE COLLATE NOCASE

contact_categories

Links contacts to categories. Many to many.

ColumnTypeConstraints and defaults
contact_idINTEGERNOT NULL REFERENCES contacts (id) ON DELETE CASCADE
category_idINTEGERNOT NULL REFERENCES categories (id) ON DELETE CASCADE

activities

Communication and history per contact: calls, texts, messages, emails, meetings, notes and stage changes. Channel and direction say where it went and which way.

ColumnTypeConstraints and defaults
idINTEGERPRIMARY KEY AUTOINCREMENT
contact_idINTEGERNOT NULL REFERENCES contacts (id) ON DELETE CASCADE
typeTEXTNOT NULL
directionTEXT 
channelTEXT 
summaryTEXT 
occurred_atTEXTNOT NULL DEFAULT (datetime('now'))
created_atTEXTNOT NULL DEFAULT (datetime('now'))

prospects

Inbound sales opportunities, one per contact. The stage drives the pipeline board.

ColumnTypeConstraints and defaults
idINTEGERPRIMARY KEY AUTOINCREMENT
contact_idINTEGERNOT NULL UNIQUE REFERENCES contacts (id) ON DELETE CASCADE
stageTEXTNOT NULL DEFAULT 'new' CHECK (stage IN ('new', 'contacted', 'qualifying', 'proposal_sent', 'follow_up', 'won', 'lost'))
sourceTEXT 
service_interestTEXT 
entity_typeTEXT 
year_endTEXT 
annual_revenueTEXT 
bookkeeping_systemTEXT 
est_valueREAL 
next_stepTEXT 
next_step_dueTEXT 
created_atTEXTNOT NULL DEFAULT (datetime('now'))
updated_atTEXTNOT NULL DEFAULT (datetime('now'))

external_profiles

Directory and social profiles the firm maintains, with review dates.

ColumnTypeConstraints and defaults
idINTEGERPRIMARY KEY AUTOINCREMENT
nameTEXTNOT NULL
urlTEXT 
categoryTEXT 
last_reviewed_atTEXT 
review_interval_daysINTEGERNOT NULL DEFAULT 90
notesTEXT 
created_atTEXTNOT NULL DEFAULT (datetime('now'))
updated_atTEXTNOT NULL DEFAULT (datetime('now'))

users

Sign-in accounts. The email address is the username. Passwords are stored as salted hashes.

ColumnTypeConstraints and defaults
idINTEGERPRIMARY KEY AUTOINCREMENT
emailTEXTNOT NULL UNIQUE COLLATE NOCASE
password_hashTEXT 
password_saltTEXT 
password_iterationsINTEGER 
failed_loginsINTEGERNOT NULL DEFAULT 0
locked_untilTEXT 
last_login_atTEXT 
created_atTEXTNOT NULL DEFAULT (datetime('now'))
updated_atTEXTNOT NULL DEFAULT (datetime('now'))

password_resets

Single-use password reset tokens. Only a SHA-256 hash of each token is stored.

ColumnTypeConstraints and defaults
idINTEGERPRIMARY KEY AUTOINCREMENT
user_idINTEGERNOT NULL REFERENCES users (id) ON DELETE CASCADE
token_hashTEXTNOT NULL UNIQUE
expires_atTEXTNOT NULL
used_atTEXT 
created_atTEXTNOT NULL DEFAULT (datetime('now'))

Services and APIs

ServiceUsed forConfigured where
Cloudflare Pages FunctionsRuns the API at /api/*Pages project crm, file functions/api/[[route]].js
Cloudflare D1Database storageBinding DB to biz-dev-crm
Cloudflare TurnstileBot check on sign-in, forgot password and resetSite key in index.html (public). Secret TURNSTILE_SECRET_KEY (Pages secret)
Cloudflare Email Sending (REST)Outreach emails and password reset emails, sent from advisors@fogler.proEndpoint /accounts/{account}/email/sending/send. Secret CF_EMAIL_TOKEN. Domain fogler.pro, records on the cf-bounce subdomain
CRM access tokenBearer token for scripts and the APIPages secret CRM_TOKEN
cdnjs (Quill)Rich text editor, front endindex.html, version 1.3.7
challenges.cloudflare.comTurnstile widget scriptindex.html
Planned, not built: Gmail APISent and received mail for advisors@fogler.proNeeds Google Workspace OAuth
Planned, not built: Google People APIGoogle Contacts importNeeds Google Workspace OAuth
Planned, not built: LinkedIn API, Meta Graph APISocial publishing (LinkedIn, Facebook, Instagram)Needs app credentials for each platform
Planned, not built: Google Voice APICalls and texts through Google Voice. Google has no public calling or texting APINeeds a workaround or another calling provider
Planned, not built: SMS provider API (e.g. Twilio)Plain text messagesNeeds a provider account
Planned, not built: WhatsApp Business Platform, Messenger Platform, Instagram MessagingMessages on each platform (Meta)Needs a Meta app and business verification
Not available: iMessageMessages from Apple devicesApple publishes no API for iMessage
Planned, not built: LinkedIn Messaging APILinkedIn direct messagesPartner access only

Secret values are never written to the repository or to this page. They live in the Cloudflare Pages settings.

API endpoints

MethodPathPurpose
POST/api/auth/loginSign in. Needs email, password and Turnstile token
POST/api/auth/forgotEmail a reset link. Same reply whether or not the account exists
POST/api/auth/resetSet a new password from a reset link
POST/api/auth/logoutEnd the session
GET/api/dashboardCounts, pipeline stages, profiles due, upcoming follow-ups
GET, POST/api/contactsList with filters (q, relationship, category), or create
GET, PATCH, DELETE/api/contacts/:idContact detail with categories, activities and prospect; update; delete
POST/api/contacts/:id/activitiesLog a call, email, meeting, note or other activity
GET, POST/api/categoriesList or create categories
DELETE/api/categories/:idDelete a category
GET, POST/api/prospectsList pipeline entries, or create one (from an existing contact or a new one)
PATCH/api/prospects/:idUpdate fields or change stage. Stage won makes the contact a customer
GET, POST/api/profilesList external profiles with review status, or create one
PATCH, DELETE/api/profiles/:idUpdate or delete a profile
POST/api/profiles/:id/reviewedMark a profile as reviewed today
POST/api/outreach/sendSend one email. Requires "confirm": true. Optional html alongside the required text

All endpoints except /api/auth/* need a valid session cookie or the CRM_TOKEN bearer token.

Security

  • Passwords: salted PBKDF2-SHA256 with 100,000 iterations. Stored as hashes only.
  • Five failed sign-ins lock the account for 15 minutes.
  • Reset links expire after one hour and work once. Only a SHA-256 hash of each link is stored.
  • Sessions: a signed cookie that is HttpOnly, Secure and SameSite=Strict, lasting 30 days.
  • Email sends require an explicit confirm flag. The From address is fixed to advisors@fogler.pro.
  • Merged contact values are HTML-escaped before they are placed in an email or the page.

Screen map

Every screen and its breadcrumb path. Contact pages add the contact's name as the last item.

ScreenBreadcrumb path

Build and deploy

  • Edit index.html, functions/api/[[route]].js or db/schema.sql.
  • Commit and push to the working branch to get a preview deployment.
  • Merge the branch to main to deploy production.
  • After a schema change, apply the new statements to biz-dev-crm. Existing data is kept.

Add external profile