Overview
Contacts are the core entity in folksbase. Each contact belongs to a workspace and has standard fields (email, name, phone, company, notes) plus a flexiblecustom_fields JSONB column for anything else. Contacts can be tagged, searched, filtered, and exported.
All contact operations go through the layered architecture: Route → Service → Repository → Drizzle. The API is fully documented via OpenAPI at /api/docs.
Creating Contacts
Contacts can be created in two ways:- Manually via the UI or
POST /api/contacts - Bulk via CSV import (see CSV Import)
- Normalizes the email address (lowercase, trimmed)
- Fetches a Gravatar avatar using an MD5 hash of the email (2-second timeout, graceful fallback to
null) - Invalidates the workspace stats cache
Listing & Pagination
TheGET /api/contacts endpoint returns a paginated list using cursor-based pagination (never OFFSET). The response shape:
nextCursor as the cursor query parameter to fetch the next page. The default page size is 50, configurable up to 100 via the limit parameter.
Search
Thesearch query parameter performs a case-insensitive ILIKE search across three fields simultaneously:
emailfirst_namelast_name
Filtering
By Tags
Pass one or more tag IDs as a comma-separatedtag_ids query parameter. The filter uses an ANY match — contacts with at least one of the specified tags are returned.
total count in the response reflects the filtered count (using COUNT(DISTINCT contact.id) to avoid double-counting contacts with multiple matching tags).
By Unsubscribe Status
Updating Contacts
PATCH /api/contacts/:id accepts partial updates. Only the fields you send are modified.
If the email changes, the system automatically re-fetches the Gravatar avatar for the new address.
Deleting Contacts
Two options:- Single delete —
DELETE /api/contacts/:idreturns 204 - Bulk delete —
POST /api/contacts/bulk-deleteaccepts up to 500 IDs per request
Sending Emails
You can send a one-off email to any contact viaPOST /api/contacts/:id/send-email:
Tagging
Contacts support a many-to-many relationship with tags through thecontact_tags junction table. See the Tags guide for details on creating and assigning tags.
When listing contacts, tags are fetched in a single batch query (not N+1) and included in the response as a tags array on each contact.
Custom Fields
Any CSV column that doesn’t map to a standard field is stored in thecustom_fields JSONB column. These fields are:
- Preserved during upserts
- Included in CSV exports
- Indexed with a GIN index for query performance