← back to Lawyer Directory Builder
docs/COMMUNITY_PLATFORM_SPEC.md
185 lines
# Lawyer Directory — Community + Ratings Platform Spec
**Status:** DRAFT — pending Steve's confirms on price/Reddit/scope, then claude-codex debate, then ralph scaffold.
## Vision
Turn the lawyer directory from a sales tool into a two-sided marketplace:
- **Clients** find + research + rate lawyers
- **Lawyers** claim + curate their profiles, respond to clients, network with peers
- Steve (admin) oversees + monetizes via subscriptions + the existing $499 EZ Upgrade site-rebuild offer
## Four user tiers (lawyer variant)
| Tier | Identity | Auth | Capabilities | Price |
|------|----------|------|--------------|-------|
| **Guest** | anonymous | none | browse firms, read public bios, see ratings | free |
| **Client** | individual | Google OAuth (free signup) | rate lawyers, post in client-side community, DM other clients, toggle comments on their own reviews | free |
| **Lawyer** | claimed-firm-member | Google OAuth + claim-via-bar# | edit own profile content, respond to client reviews, DM clients, DM peers, paid tier required | **$29/mo** (default; needs Steve confirm) |
| **Admin** | Steve only | hardcoded | see every firm + every pitch URL + every dashboard, manage takedowns, override ratings, send broadcast pitches | n/a |
**Auth strategy:** Google OAuth via existing `google-oauth-integration` skill — single provider, simplest UX, no password reset hell. Lawyer tier additionally requires bar-number verification (we already have 88,511 bar #s in `professionals` table; lawyers paste their bar# at signup, system matches against `professionals.bar_number` + sends verification email to address on file at CalBar).
## Schema additions
```sql
-- 013_user_tiers.sql
ALTER TABLE app_users
ADD COLUMN tier TEXT NOT NULL DEFAULT 'client' CHECK (tier IN ('guest','client','lawyer','admin')),
ADD COLUMN google_sub TEXT UNIQUE, -- Google OAuth subject ID
ADD COLUMN claimed_professional_id BIGINT REFERENCES professionals(id),
ADD COLUMN bar_number TEXT, -- self-reported, verified
ADD COLUMN bar_verified_at TIMESTAMPTZ,
ADD COLUMN paid_until TIMESTAMPTZ, -- subscription expiry
ADD COLUMN stripe_customer_id TEXT,
ADD COLUMN stripe_subscription_id TEXT,
ADD COLUMN comments_enabled BOOLEAN NOT NULL DEFAULT true; -- per-user toggle
-- 014_ratings.sql
CREATE TABLE ratings (
id BIGSERIAL PRIMARY KEY,
professional_id BIGINT REFERENCES professionals(id) ON DELETE CASCADE,
organization_id BIGINT REFERENCES organizations(id) ON DELETE CASCADE,
reviewer_user_id BIGINT REFERENCES app_users(id) ON DELETE SET NULL,
source TEXT NOT NULL CHECK (source IN ('user','avvo','google','yelp','reddit','manual')),
source_url TEXT,
stars NUMERIC(2,1) NOT NULL CHECK (stars BETWEEN 0 AND 5),
service_score NUMERIC(2,1), -- responsiveness, communication
price_score NUMERIC(2,1), -- value
quality_score NUMERIC(2,1), -- outcome
comment TEXT,
posted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT rating_target_check CHECK (professional_id IS NOT NULL OR organization_id IS NOT NULL)
);
CREATE INDEX idx_ratings_pro ON ratings(professional_id);
CREATE INDEX idx_ratings_org ON ratings(organization_id);
-- 015_community.sql
CREATE TABLE threads (
id BIGSERIAL PRIMARY KEY,
channel TEXT NOT NULL CHECK (channel IN ('client_public','lawyer_public','dm_client_lawyer','dm_client_client','dm_lawyer_lawyer')),
participant_user_ids BIGINT[] NOT NULL, -- for DMs; for public channels = empty
topic TEXT,
organization_id BIGINT REFERENCES organizations(id), -- threads scoped to a firm
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_message_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_threads_channel ON threads(channel, last_message_at DESC);
CREATE INDEX idx_threads_participants ON threads USING GIN (participant_user_ids);
CREATE TABLE messages (
id BIGSERIAL PRIMARY KEY,
thread_id BIGINT NOT NULL REFERENCES threads(id) ON DELETE CASCADE,
user_id BIGINT NOT NULL REFERENCES app_users(id) ON DELETE SET NULL,
body TEXT NOT NULL,
posted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
edited_at TIMESTAMPTZ,
hidden_at TIMESTAMPTZ -- soft-delete for moderation
);
CREATE INDEX idx_messages_thread ON messages(thread_id, posted_at);
```
## Public-rating ingestion (no Reddit, per Steve confirm pending)
Sources to scrape ONCE per firm, store with `ratings.source` set:
- **Avvo** — has lawyer-specific ratings 1–10, public profile pages
- **Google Business** — star + review count via Places API ($)
- **Yelp Fusion API** — star + review count, free tier (5k req/day)
- (skipped: Reddit — too high-risk per default)
For each firm with a website, the ingester:
1. Searches Google for `"<firm name>" site:avvo.com` → first profile result
2. Scrapes the rating + review count
3. Inserts into `ratings` with `source='avvo'`, `reviewer_user_id=NULL`
4. Same for Yelp (via API), Google (via Places API if budget allows)
Aggregate ranking formula (initial):
```
overall_score =
0.5 * weighted_avg(stars across all sources, weight=review_count)
+ 0.2 * service_score (avg of user-submitted)
+ 0.2 * quality_score (avg of user-submitted)
+ 0.1 * price_score (avg of user-submitted)
```
Cold-start: firms with 0 user reviews use only the public-source weighted avg.
## Pages to build
### Guest-visible
- `/firms/:id` — firm public page (existing data + ratings widget + "claim this firm" CTA for unclaimed)
- `/lawyers/:id` — individual attorney page (bio + ratings + practice areas)
- `/community` — read-only feed of public threads (paywall preview at message #5)
### Client-tier
- `/community` — full read + post in client-public channel
- `/community/dm` — initiate DM with another client OR lawyer
- `/profile` — manage account, toggle `comments_enabled`, see all reviews they've posted
- `/firms/:id/rate` — leave a star rating + dimension scores + comment
### Lawyer-tier (paid)
- `/dashboard/lawyer` — claimed firm overview, recent reviews, unread DMs
- `/dashboard/lawyer/edit` — edit own bio, practice areas, photo, contact
- `/dashboard/lawyer/respond` — reply to client reviews (one reply per review)
- `/community` — post in lawyer-public + DM peers + DM clients
- `/billing` — Stripe portal
### Admin (Steve only)
- `/admin/firms` — every firm + audit score + has-mockup + has-claim flag
- `/admin/pitches` — every per-firm pitch URL `/p/:id` with status (sent / opened / replied)
- `/admin/users` — every user, force-tier-change, refund button
- `/admin/broadcast` — bulk-pitch composer (template + recipient filter from dashboard)
- `/admin/moderation` — flagged reviews/messages queue
## Pitch persona
**From:** Steve Abrams `<hello@agentabrams.com>`
**Signature:**
> Steve Abrams
> Entrepreneur · Designer · Small-business owner
> agentabrams.com
Pitch email body (template, per firm) — already drafted in `/p/:firm_id` page; outbound version uses same screenshot+mockup layout in HTML email.
## Build phases (for ralph)
**Phase 1 — Auth + Tiers (week 1)**
- Migration 013, 014, 015
- Google OAuth integration (use existing skill)
- Tier middleware + route guards
- Admin user seeded via env
**Phase 2 — Ratings (week 2)**
- Public-source rating ingester (Avvo/Yelp/Google)
- Star-rating UI on firm + attorney pages
- Comment toggle on user profile
**Phase 3 — Community (week 3)**
- Threads + messages schema + REST routes
- Public client + lawyer feeds
- DM channels (client-lawyer, peer-peer)
**Phase 4 — Stripe paywall (week 4)**
- Subscription product creation
- Webhook handlers (payment_succeeded, subscription_canceled)
- Tier upgrade flow
**Phase 5 — Lawyer-claim flow (week 4–5)**
- Bar-number verification email (PurelyMail send via hello@agentabrams.com)
- Claim approval (auto if email matches CalBar; manual queue otherwise)
**Phase 6 — Admin pages (ongoing)**
- Stats + moderation + broadcast pitch composer
## Open questions (need Steve)
1. ⏳ **Lawyer subscription price** — $29/mo default, confirm or change
2. ⏳ **Reddit scraping** — default = SKIP, use Avvo+Yelp+Google only. Confirm.
3. ⏳ **Vertical scope** — default = build inside `lawyer-directory-builder` (no platform extraction). Confirm.
4. ⏳ **Google OAuth client** — use existing `dw-sso-integration` skill or fresh OAuth app for `lawyers.agentabrams.com`?
5. ⏳ **Public domain for client signups** — `lawyers.agentabrams.com` is already configured per CNCP. Use that, or new subdomain `directory.agentabrams.com`?
## What happens NOW (no confirms needed)
- Mac1 keeps generating mockups in background (independent of this spec)
- This spec waits for Steve's confirms
- Then `/claude-codex` is invoked to debate the architecture (specifically: tier middleware shape, message-thread schema choice, public-rating attribution legality)
- Then `/ralph` is invoked with the phase-by-phase task list above to scaffold autonomously