9.6 KiB
Going Live — JQC Billing & Email Checklist
Steps to complete before accepting real paying customers.
Follow sections in order: Stripe first, then email, then cron, then verify.
1. Switch Stripe from Test → Live Mode
1.1 Get your live API keys
- Log in to dashboard.stripe.com
- Click the toggle in the top-left from Test mode → Live mode
- Go to Developers → API keys
- Copy:
- Secret key — starts with
sk_live_… - Publishable key — starts with
pk_live_…
- Secret key — starts with
1.2 Create live Products and Price IDs
You must do this in live mode — test-mode prices are not valid for real payments.
- Go to Products → + Add product
- Create three products: Starter, Pro, Enterprise
- For each, add a recurring price (monthly)
- Copy each Price ID — starts with
price_live_…(NOTprod_…)
1.3 Update /etc/jqc/app.env
sudo nano /etc/jqc/app.env
Replace the test values:
STRIPE_SECRET_KEY=sk_live_XXXXXXXXXXXXXXXXXXXXXXXX
STRIPE_PUBLISHABLE_KEY=pk_live_XXXXXXXXXXXXXXXXXXXXXXXX
STRIPE_PRICE_STARTER=price_live_XXXXXXXXXXXXXXXXXXXXXXXX
STRIPE_PRICE_PRO=price_live_XXXXXXXXXXXXXXXXXXXXXXXX
STRIPE_PRICE_ENTERPRISE=price_live_XXXXXXXXXXXXXXXXXXXXXXXX
1.4 Seed the live Price IDs into the control DB
set -a; . /etc/jqc/app.env; . /etc/jqc/control.env; set +a
python -m control.cli seed
python -m control.cli list-plans # verify all three show price_live_... IDs
1.5 Register the live Stripe webhook
- In Stripe Dashboard (live mode) → Developers → Webhooks → + Add endpoint
- Endpoint URL:
https://yourdomain.jqc.app/billing/webhook(use your tenant-zero domain or the primary custom domain) - Events to listen to (select these exactly):
invoice.payment_failedcustomer.subscription.updatedcustomer.subscription.deletedcheckout.session.completed
- Click Add endpoint
- Copy the Signing secret — starts with
whsec_…
1.6 Update webhook secret
sudo nano /etc/jqc/app.env
STRIPE_WEBHOOK_SECRET=whsec_XXXXXXXXXXXXXXXXXXXXXXXX
1.7 Restart the app
sudo systemctl daemon-reload
sudo systemctl restart jqc
1.8 Verify webhook delivery
- In Stripe Dashboard → Webhooks → click your endpoint
- Send a test event:
invoice.payment_failed - Check your app logs:
tail -f /var/www/jqc/logs/jqc.log | grep BILLING - You should see:
BILLING WEBHOOK | event=invoice.payment_failed
2. Email Deliverability (SPF, DKIM, DMARC)
Without these DNS records, billing emails (trial ending, payment failed, etc.) will land in spam or be rejected outright.
2.1 SPF Record
SPF tells receiving servers which servers are allowed to send email for your domain.
Add a TXT record on your sending domain (e.g. jqc.app or your custom domain):
| Name | Type | Value |
|---|---|---|
@ |
TXT | v=spf1 include:YOUR-SMTP-PROVIDER.com ~all |
Replace YOUR-SMTP-PROVIDER.com with your actual SMTP provider's include directive:
| Provider | Include value |
|---|---|
| SendGrid | include:sendgrid.net |
| Mailgun | include:mailgun.org |
| Amazon SES | include:amazonses.com |
| Google Workspace | include:_spf.google.com |
| Your own server | ip4:YOUR.SERVER.IP.ADDRESS |
Example for your own server:
v=spf1 ip4:123.45.67.89 ~all
Example for SendGrid:
v=spf1 include:sendgrid.net ~all
⚠️ Only one SPF record per domain is allowed. If one already exists, append to it.
2.2 DKIM Record
DKIM cryptographically signs outgoing emails so recipients can verify they're authentic.
If using SendGrid / Mailgun / Amazon SES:
- Follow their dashboard instructions to generate a DKIM key pair
- They will give you TXT records to add (usually at
mail._domainkey.yourdomain.com) - Add those records exactly as provided
If sending from your own server (Postfix + OpenDKIM):
# Install OpenDKIM
sudo apt install opendkim opendkim-tools
# Generate key pair
sudo mkdir -p /etc/opendkim/keys/yourdomain.com
sudo opendkim-genkey -b 2048 -d yourdomain.com -D /etc/opendkim/keys/yourdomain.com -s mail -v
# View the TXT record to add to DNS
sudo cat /etc/opendkim/keys/yourdomain.com/mail.txt
Add the output as a TXT record at mail._domainkey.yourdomain.com.
2.3 DMARC Record
DMARC tells receiving servers what to do with emails that fail SPF/DKIM checks, and where to send failure reports.
Add a TXT record:
| Name | Type | Value |
|---|---|---|
_dmarc |
TXT | v=DMARC1; p=quarantine; pct=100; rua=mailto:dmarc@yourdomain.com |
Start with p=none (monitoring only) for the first 2 weeks, then move to p=quarantine:
# Week 1-2: monitor only
v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com
# After confirming SPF and DKIM pass in reports:
v=DMARC1; p=quarantine; pct=100; rua=mailto:dmarc@yourdomain.com
2.4 Test email deliverability
Once DNS records propagate (up to 48 hours), test with:
- mail-tester.com — free, shows your score out of 10 and exactly what's failing
- MXToolbox — checks SPF, DKIM, DMARC, blacklists
Target score: 9/10 or higher before sending billing emails to customers.
2.5 Verify from the app
# Send a test billing email from Flask shell
cd /var/www/jqc
source venv/bin/activate
flask shell
from app.billing.emails import send_billing_email
send_billing_email(
'your@email.com',
'trial_ending',
{
'trial_ends_at': 'July 15, 2026',
'subscribe_url': 'https://demo.jqc.app/billing/subscribe',
'days_left': 3,
'tenant_name': 'Test Tenant',
}
)
exit()
Check that it arrives in the inbox (not spam) and renders correctly.
3. Cron Jobs — Final Setup
After going live, make sure all cron jobs including the new trial reminder are configured.
Edit the crontab for the jqc user:
sudo crontab -u jqc -e
Add all six jobs (replace YOUR_SECRET and yourdomain.jqc.app):
# SLA breach alerts — every 30 minutes
*/30 * * * * curl -s -X POST https://yourdomain.jqc.app/notifications/check-sla \
-d "token=YOUR_DIGEST_SECRET" >> /var/www/jqc/logs/cron.log 2>&1
# Daily email digest — 7 AM
0 7 * * * curl -s -X POST https://yourdomain.jqc.app/notifications/send-digest \
-d "token=YOUR_DIGEST_SECRET&frequency=daily" >> /var/www/jqc/logs/cron.log 2>&1
# Weekly email digest — Monday 7 AM
0 7 * * 1 curl -s -X POST https://yourdomain.jqc.app/notifications/send-digest \
-d "token=YOUR_DIGEST_SECRET&frequency=weekly" >> /var/www/jqc/logs/cron.log 2>&1
# Facility score trend alerts — 8 AM daily
0 8 * * * curl -s -X POST https://yourdomain.jqc.app/notifications/check-score-trends \
-d "token=YOUR_DIGEST_SECRET" >> /var/www/jqc/logs/cron.log 2>&1
# Scheduled reports — 8 AM daily
0 8 * * * curl -s -X POST https://yourdomain.jqc.app/scheduled-reports/run \
-d "secret=YOUR_DIGEST_SECRET" >> /var/www/jqc/logs/cron.log 2>&1
# Expired API token cleanup — 3 AM daily
0 3 * * * curl -s -X POST https://yourdomain.jqc.app/notifications/cleanup-tokens \
-d "token=YOUR_DIGEST_SECRET" >> /var/www/jqc/logs/cron.log 2>&1
# Trial-ending reminder emails — 9 AM daily ← NEW
0 9 * * * curl -s -X POST https://yourdomain.jqc.app/notifications/trial-reminders \
-d "token=YOUR_DIGEST_SECRET" >> /var/www/jqc/logs/cron.log 2>&1
# Per-tenant DB backup — 2 AM daily
0 2 * * * set -a; . /etc/jqc/control.env; set +a; \
python -m control.backup --tenant all \
--output-dir /var/backups/jqc >> /var/www/jqc/logs/cron.log 2>&1
Note on
yourdomain.jqc.app: Use your tenant-zero subdomain (e.g.lts.jqc.app) or your custom domain (e.g.jqc.ltservicesinc.com). The trial-reminders endpoint is exempt from tenant middleware so any valid host works for it, but the others require a valid tenant host.
4. Final Pre-Launch Verification
Run through this checklist before announcing to customers:
Stripe
python -m control.cli list-plansshowsprice_live_…IDs (notprice_test_…)- Stripe Dashboard → Webhooks shows endpoint is enabled in live mode
- Made a real test payment through the subscribe flow using a live card
- Webhook delivery log shows
200 OKresponses billing_emailis set on tenant-zero (check:python -m control.cli list-plans)
- mail-tester.com score ≥ 9/10
- SPF TXT record published and passes at mxtoolbox.com
- DKIM TXT record published and passes
- DMARC TXT record published (start with
p=none) - Test billing email arrives in Gmail inbox, not spam
- Test billing email HTML renders correctly (blue header, button, footer)
Trial reminder cron
- Test manually:
curl -X POST https://yourdomain.jqc.app/notifications/trial-reminders -d "token=YOUR_SECRET" - Response:
{"ok": true, "sent": N, "skipped": N, "errors": 0} - Cron entry is in
crontab -u jqc -l
Backup
python -m control.backup --tenant all --output-dir /var/backups/jqcruns without errors/var/backups/jqc/contains.sql.gzfiles for each tenant- Backup cron entry is in the crontab
5. Post-Launch Monitoring
Watch these in the first week after going live:
# App errors
tail -f /var/www/jqc/logs/jqc.log | grep -E 'ERROR|WARNING|BILLING'
# Cron job results
tail -f /var/www/jqc/logs/cron.log
# Stripe events (Stripe Dashboard → Developers → Events)
# Look for: checkout.session.completed, customer.subscription.updated
# Trial reminders sent
grep 'TRIAL REMINDERS' /var/www/jqc/logs/jqc.log