▌ RezMyCV.md
▒ PATH:
▒ SIZE: 6.8 KB
▒ MODIFIED: 2026-06-03 02:01
RezMyCV.md▒ SIZE: 6.8 KB
▒ MODIFIED: 2026-06-03 02:01
# RezMyCV — AI-Powered CV Tailoring SaaS
**Website:** https://rezmycv.com
**GitHub:** ncik8/TailorMyCV
**Stack:** Flask + Railway + Supabase + Stripe
**Status:** Live and functional (June 2026)
---
## What It Does
An AI-powered SaaS that helps job seekers tailor their CV to specific job descriptions. Users upload their CV, paste a job description URL, and get an ATS-optimized CV plus cover letter.
### Core Features
- CV upload + AI parsing
- Job description input (paste URL or text)
- Gap analysis Q&A (identifies missing skills)
- Tailored CV generation (ATS-optimized)
- Cover letter generation (Pro+ tier)
- PDF download
- Profile editing (name, title, experience, education, skills, languages)
### Pricing Tiers
| Tier | Price | Features |
|------|-------|----------|
| Free | $0 | 10 CV generations, tailored templates, Gap Q&A, PDF download |
| Pro | $19/mo | Unlimited CV generations, priority support |
| Pro+ | $25/mo | Everything in Pro + unlimited tailored cover letters |
---
## Tech Stack
### Backend
- **Flask** (Python 3.9+)
- **Routes:** Auth, CV upload/edit, Job paste, Gap analysis, Tailor, Dashboard, Upgrade, Stripe webhook
- **Sessions:** Minimal — only `user_id` stored, all data fetched from Supabase per request
- **API keys:** Stored in Railway env vars
### Database (Supabase)
- **URL:** `https://tfkzirfxixlylizlmxeur.supabase.co`
- **Tables:**
- `users` — id, email, created_at
- `profiles` — user_id, tier, cv_count, stripe_customer_id, stripe_subscription_id, stripe_subscription_status
- `user_cvs` — user_id, name, title, email, phone, location, linkedin, summary, experience (JSON), education (JSON), skills (JSON), certifications (JSON), languages (JSON), projects (JSON), additional_info, raw_text, gap_answers (JSON)
- `job_descriptions` — user_id, url, title, company, description, requirements (JSON), gap_questions (JSON), gap_answers (JSON), created_at
### Stripe Integration
- **Products:** Test Pro ($1/HKD), Test Pro+ ($1.50/HKD) — demo prices for testing
- **Real prices:** Pro $19/mo, Pro+ $25/mo
- **Checkout:** `/checkout/<tier>` creates Stripe Checkout session
- **Webhooks:** `/stripe/webhook` handles `checkout.session.completed`, `customer.subscription.created/updated/deleted`
- **Upgrade flow:** `/upgrade-subscription/<tier>` uses `subscriptions.update()` to modify existing subscription (no duplicate)
- **Cancel handling:** `customer.subscription.deleted` sets tier back to 'free', clears stripe_subscription_id and stripe_customer_id
### Key Files
- `app.py` — main Flask app, all routes
- `services/stripe_client.py` — Stripe helpers (create_checkout_session, construct_webhook_event, upgrade_subscription)
- `services/user_cv.py` — load_cv, save_cv, parse_cv
- `services/gap_analyzer.py` — generate_gap_questions, answer_gap
- `services/tailor.py` — tailor_cv, optimize_cv (ATS keyword matching)
- `services/cv_parser_ai.py` — AI parsing via MiniMax
- `templates/` — HTML templates (dashboard, upgrade, edit_profile, gap_analyze, gap_answer, tailored_cv, etc.)
---
## Stripe Payment Flow (Fixed June 2026)
### The Problem
Early Stripe integration had multiple bugs:
1. Webhook used `obj.get('subscription')` on Stripe objects — Stripe objects don't have `.get()` method → AttributeError
2. Session cached stale tier values — didn't read fresh tier from Supabase on each page load
3. Upgrade created duplicate subscriptions instead of modifying existing one
### The Fixes
1. Webhook handler uses `getattr(obj, 'subscription', None) or (obj['subscription'] if 'subscription' in obj else None)` — works for both dict-like and object-like Stripe responses
2. `init_session()` now fetches fresh tier from Supabase `profiles` table on every page load
3. Upgrade flow checks subscription status — if `incomplete_expired` or `canceled`, clears dead subscription first, then creates new one
4. `customer.subscription.deleted` webhook clears `stripe_customer_id` too (was missing)
### Upgrade Flow Logic
```
User on Pro → clicks "Upgrade to Pro+" →
Check existing subscription status →
If active: modify price via subscriptions.update() (proration=create_prorations)
If dead: clear subscription_id + redirect to checkout
If none: redirect to checkout
```
---
## DNS Setup (June 2026)
### Registrar: name.com
- Domain: rezmycv.com, renews Apr 25, 2027, $14/yr
### Nameservers: Cloudflare
- `oswald.ns.cloudflare.com`
- `paislee.ns.cloudflare.com`
- Switched from Railway's default nameservers
### DNS Records (Cloudflare)
- A record `@` → `192.0.2.1` (proxied) — Cloudflare CNAME flattening for apex
- CNAME `www` → `yqc5rj4s.up.railway.app` (proxied)
- TXT `_railway-verify` → `railway-verify=14c7a2cc582b682d2e576f345358ef62fbd4027d73498ef879038869e6307948` (grey cloud, for Railway SSL)
### Railway App Domain
- `yqc5rj4s.up.railway.app`
---
## Email Setup
### Cloudflare Email Routing
- Free email forwarding via Cloudflare
- `hello@rezmycv.com` → personal email (configured in Cloudflare dashboard)
- Also set up ForwardMX as backup
---
## Bug Fixes Log
| Date | Bug | Fix |
|------|-----|-----|
| Jun 2, 2026 | Webhook `obj.get('subscription')` → AttributeError | Use `getattr()` with fallback |
| Jun 2, 2026 | Session cached stale tier | `init_session()` now reads from Supabase |
| Jun 2, 2026 | Pro → Pro+ created duplicate subscription | `upgrade_subscription()` modifies existing via `subscriptions.update()` |
| Jun 2, 2026 | Cancel didn't clear stripe_customer_id | Added to webhook handler |
| Jun 2, 2026 | "Current Plan" showed on wrong tiers | Use template `tier` var (fresh DB), not `session.get('tier')` |
| Jun 2, 2026 | Dashboard name showed "Test Name}" | Extra `}` removed from template (was visual display bug in browser) |
| Jun 2, 2026 | Landing page "30 seconds" | Changed to "seconds" |
---
## Marketing Plan (June 2026)
### Target Audience
Job seekers who are tired of mass-applying with generic CVs
### Content Strategy
- **Positioning:** "EasyApply is broken — do the work" / "Effort beats automation"
- **RezMyCV is #1** in all content as the solution
### Distribution Channels
1. **CoinCUstrard** (Nick's news channel) — Day 1 article
2. **News outlets** — Day 2+ article (different angle)
3. **YouTube Long** (12k followers) — Day 2-3
4. **YouTube Shorts** — Day 3-4
5. **Reddit** — Day 4-5 (Nick posts genuine story → Henry handles comments)
6. **X** — Throughout 2 weeks (3-4 posts)
### Article Angle
"10 Job Sites That Require Effort (And Why RezMyCV is #1)" — targeting people who clicked EasyApply 300 times with no interviews, positioning RezMyCV as the "effort" alternative that actually works.
### Reddit Story Hook
"300 EasyApply → 0 interviews. 100 with RezMyCV → 7 interviews."
---
## Related Notes
- [[Projects]] — project index
- [[RezMyCV Content Calendar]] — content rollout schedule