mj8v7vygpb-max.✅ You can log in with a 2FA code, and a recovery email is verified.
jadin-jones).✅ The repo shows under the org and two owners exist.
git --version and node --version both print a version.git clone https://github.com/<your-org>/JJ-playbook-ios.git
cd JJ-playbook-ios
Create a file named .gitignore with exactly:
node_modules/
.DS_Store
.env
Remove the junk that shouldn't be tracked:
git rm -r --cached node_modules
git rm --cached .DS_Store
git add .gitignore
git commit -m "Clean repo: ignore node_modules and .DS_Store"
git push
✅ On GitHub, node_modules and .DS_Store are gone from the file list.
Create your safe work branch (main stays the live site):
git checkout -b develop
git push -u origin develop
✅ GitHub's branch dropdown shows both main and develop.
JJ-playbook-ios.build (or . if files sit at the repo root). Click Deploy.*.netlify.app link and confirm the app loads. This is your PROD site.Netlify → Add new site → Import → same repo → set Branch to deploy = develop → Deploy. This is your DEV site.
On both sites: Site configuration → Environment variables → Add a variable, and add:
ANTHROPIC_API_KEY — your Anthropic key (console.anthropic.com → API keys)GRANOLA_SECRET — a brand-new random string (see below)FIREBASE_PROJECT — test-6b2ab (or the dev project on the Dev site)FIREBASE_API_KEY — Firebase → Project settings → Web API keyCOACH_EMAIL — lucas@jadin-jones.comGenerate a strong secret and paste it as the value, then set the same value in Granola/Zapier:
openssl rand -hex 24Open https://<your-dev-site>.netlify.app/api/coach in a browser. A JSON error like “POST only” is fine — it proves the function is alive. Do the same for /api/granola.
GitHub repo → Settings → Branches → Add branch protection rule → Branch name pattern main → check Require a pull request before merging → Create.
Now nothing reaches the live site by accident.
jjplaybook.jadin-jones.com → copy the target Netlify shows (e.g. your-prod-site.netlify.app).dev.jjplaybook.jadin-jones.com → copy its target.GoDaddy → My Products → jadin-jones.com → DNS → Add two records:
jjplaybook, Value your-prod-site.netlify.appdev.jjplaybook, Value your-dev-site.netlify.appWait for DNS (up to 24–48h). Check progress:
nslookup jjplaybook.jadin-jones.com
When it returns Netlify's address, HTTPS turns on automatically. ✅ Both https:// URLs load with a padlock.
GoDaddy → jadinjones.com → Domain settings → Forwarding → Add → forward to https://jadin-jones.com, type Permanent (301), Masking OFF; repeat for www. ✅ Visiting jadinjones.com lands on jadin-jones.com.
Your app loads Firebase's database/messaging pieces but not login. Add the auth SDK matching the same Firebase version you already use (keep versions identical or they conflict). Place it right after your existing firebase-app-compat script:
<script src="https://www.gstatic.com/firebasejs/10.12.5/firebase-auth-compat.js"></script>
✅ In the browser console, typing firebase.auth returns a function (not “undefined”).
Rules can't read the email hidden in the text blob, so every member record must carry a plain ownerEmail. Find where records save — search your source for collection(COLL).doc(.
Helper (place once, near your Firebase setup):
function jjOwnerEmail() {
var u = firebase.auth().currentUser;
return u && u.email ? u.email.toLowerCase() : null;
}
At each member-record save (keys resp:, coach:, gin:), stamp the owner:
// BEFORE:
// db.collection(COLL).doc(key).set({ value: s });
// AFTER:
db.collection(COLL).doc(key).set(
{ value: s, ownerEmail: jjOwnerEmail() },
{ merge: true }
);
Do the same for the chat save ({messages: ...}). Leave program/content saves alone.
service-account.json (never commit it).backfill-owner.js:const admin = require('firebase-admin');
admin.initializeApp({ credential: admin.credential.cert(require('./service-account.json')) });
const db = admin.firestore();
const COLL = 'jj_playbook';
const isMember = id => /^(resp|coach|gin):/.test(id);
(async () => {
const snap = await db.collection(COLL).get();
let fixed = 0;
for (const doc of snap.docs) {
if (!isMember(doc.id)) continue;
const data = doc.data();
if (data.ownerEmail) continue;
let email = null;
try { email = (JSON.parse(data.value || '{}').email || '').toLowerCase(); } catch (e) {}
if (!email) { console.log('no email in', doc.id); continue; }
await doc.ref.set({ ownerEmail: email }, { merge: true });
fixed++;
}
console.log('Backfilled', fixed, 'records');
})();
Run it:
npm init -y && npm install firebase-admin
node backfill-owner.js
✅ Prints “Backfilled N records”; member docs now have ownerEmail.
Save as set-admin.js and run after Lucas has logged in once (his account must exist first):
const admin = require('firebase-admin');
admin.initializeApp({ credential: admin.credential.cert(require('./service-account.json')) });
admin.auth().getUserByEmail('lucas@jadin-jones.com')
.then(u => admin.auth().setCustomUserClaims(u.uid, { admin: true }))
.then(() => console.log('Lucas is now admin'))
.catch(e => console.error(e));
node set-admin.jsFirebase → Firestore Database → Rules → replace everything with this, then Publish. Confirm the prefixes resp|coach|gin are your only per-member keys; add any others.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function signedIn() { return request.auth != null && 'email' in request.auth.token; }
function myEmail() { return request.auth.token.email.lower(); }
function isAdmin() { return request.auth != null && request.auth.token.admin == true; }
function isMemberDoc(id) { return id.matches('^(resp|coach|gin):.*'); }
match /jj_playbook/{docId} {
allow read: if isAdmin()
|| (signedIn() && !isMemberDoc(docId))
|| (signedIn() && isMemberDoc(docId) && resource.data.ownerEmail == myEmail());
allow create, update: if isAdmin()
|| (signedIn() && isMemberDoc(docId)
&& request.resource.data.ownerEmail == myEmail()
&& (resource == null || resource.data.ownerEmail == myEmail()));
allow delete: if isAdmin();
}
}
}
✅ A member can open their own screen; a second account can't read the first's records. Test on Dev first.
FIREBASE_SERVICE_ACCOUNT (paste the whole JSON).netlify/functions/granola.js with the Admin version:const admin = require('firebase-admin');
if (!admin.apps.length) {
admin.initializeApp({
credential: admin.credential.cert(JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT))
});
}
const db = admin.firestore();
const COLL = 'jj_playbook';
async function getDoc(id) {
const s = await db.collection(COLL).doc(id).get();
if (!s.exists) return null;
try { return JSON.parse(s.data().value); } catch (e) { return null; }
}
async function setDoc(id, val) {
await db.collection(COLL).doc(id).set({ value: JSON.stringify(val) }, { merge: true });
return true;
}
async function findByEmail(email) {
const snap = await db.collection(COLL).get();
const out = [];
snap.forEach(d => {
if (!d.id.startsWith('resp:')) return;
let v = null; try { v = JSON.parse(d.data().value); } catch (e) { return; }
if (v && String(v.email || '').toLowerCase() === email) out.push(v);
});
return out;
}
Add the dependency and redeploy: npm install firebase-admin, commit, push. ✅ A test note to /api/granola still files.
function jjSignInWithGoogle() {
var provider = new firebase.auth.GoogleAuthProvider();
return firebase.auth().signInWithRedirect(provider);
}
firebase.auth().getRedirectResult().catch(function (e) { console.error(e); });
Wire jjSignInWithGoogle to a “Continue with Google” button. ✅ Test with a Workspace account and a personal Gmail.
FALL2026) with field active: true.netlify/functions/signup.js:const admin = require('firebase-admin');
if (!admin.apps.length) {
admin.initializeApp({
credential: admin.credential.cert(JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT))
});
}
const db = admin.firestore();
const CORS = { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type', 'Access-Control-Allow-Methods': 'POST, OPTIONS' };
exports.handler = async function (event) {
if (event.httpMethod === 'OPTIONS') return { statusCode: 204, headers: CORS, body: '' };
if (event.httpMethod !== 'POST') return { statusCode: 405, headers: CORS, body: '{"error":"POST only"}' };
let b; try { b = JSON.parse(event.body || '{}'); } catch (e) { return { statusCode: 400, headers: CORS, body: '{"error":"Bad JSON"}' }; }
const code = String(b.code || '').trim();
const email = String(b.email || '').trim().toLowerCase();
const name = String(b.name || '').trim().slice(0, 80);
const password = String(b.password || '');
if (!code || !email || !name || password.length < 8)
return { statusCode: 400, headers: CORS, body: JSON.stringify({ error: 'Missing code, email, name, or password (8+ chars).' }) };
const codeDoc = await db.collection('orgcodes').doc(code).get();
if (!codeDoc.exists || codeDoc.data().active !== true)
return { statusCode: 403, headers: CORS, body: JSON.stringify({ error: 'That org code is not valid.' }) };
const used = await db.collection('signups').doc(email).get();
if (used.exists)
return { statusCode: 409, headers: CORS, body: JSON.stringify({ error: 'This email has already registered. Ask an admin for help.' }) };
let user;
try {
user = await admin.auth().createUser({ email, password, displayName: name });
} catch (e) {
return { statusCode: 409, headers: CORS, body: JSON.stringify({ error: 'An account with this email already exists.' }) };
}
await db.collection('signups').doc(email).set({ code, name, uid: user.uid, ts: Date.now() });
return { statusCode: 200, headers: CORS, body: JSON.stringify({ ok: true }) };
};
Route it — add to netlify.toml:
[[redirects]]
from = "/api/signup"
to = "/.netlify/functions/signup"
status = 200
Wire your sign-up screen (collect code, email, name, password):
async function jjSignUp(code, email, name, password) {
const r = await fetch('/api/signup', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, email, name, password })
});
const data = await r.json();
if (!r.ok) throw new Error(data.error || 'Sign-up failed');
await firebase.auth().signInWithEmailAndPassword(email, password);
}
✅ Sign up once → works. Same email again → “already registered.”
function jjLogin(email, password) {
return firebase.auth().signInWithEmailAndPassword(email.trim().toLowerCase(), password);
}
function jjResetPassword(email) {
return firebase.auth().sendPasswordResetEmail(email.trim().toLowerCase());
}
Gate the whole app on being logged in (place at app start):
firebase.auth().onAuthStateChanged(function (user) {
if (user) { showApp(); }
else { showLoginScreen(); }
});Search your source for the old gate (the showGate / org-code entry path) and delete that entry route. The app now opens only via onAuthStateChanged, so the old code can't bypass real login.
Existing members have data but no login yet. Pre-create their accounts, then tell them to use “Forgot password” (or Google) to get in. Save as migrate-members.js:
const admin = require('firebase-admin');
admin.initializeApp({ credential: admin.credential.cert(require('./service-account.json')) });
const db = admin.firestore();
(async () => {
const snap = await db.collection('jj_playbook').get();
const emails = new Set();
snap.forEach(d => {
if (!/^resp:/.test(d.id)) return;
try { const e = (JSON.parse(d.data().value).email || '').toLowerCase(); if (e) emails.add(e); } catch (x) {}
});
for (const email of emails) {
try { await admin.auth().getUserByEmail(email); }
catch { await admin.auth().createUser({ email, password: Math.random().toString(36).slice(2) + 'A9!' }); console.log('created', email); }
}
console.log('Done. Members:', emails.size);
})();
node migrate-members.jsThe web Google-login flow does not work inside a native app shell. For the store apps, add the Capacitor auth plugin and follow its setup so Google sign-in works natively (email/password works either way):
npm install @capacitor-firebase/authenticationEnroll in the Apple Developer Program ($99/year) at developer.apple.com. Approval takes 1–3 days, so start this early.
Your latest commit shows a failing build (red ✗). Open codemagic.io → your app → read the failed log → fix the first error → re-run until the build is green.
npx cap sync ios
npx cap open ios
In Xcode set your Team (from the Apple account) so signing works. Then archive and upload to App Store Connect → TestFlight for testers, and submit for review.
Add Android to the project and drop your Firebase Android config (google-services.json) into android/app/:
npx cap add android
npx cap sync androidCreate a Google Play Developer account ($25 one-time) at play.google.com/console.
Build a signed app bundle (create a keystore once and keep it safe):
npx cap open android
# In Android Studio: Build → Generate Signed Bundle/APK → Android App Bundle
Then Play Console → Create app → upload the .aab to Internal testing → test → promote to Production.