Create a Firebase project
- Go to console.firebase.google.com and sign in with a Google account.
- Tap Create a project, name it estimate-lot.
- Turn Google Analytics off (not needed), then Create project.
Turn on email sign-in
- In the left menu: Build → Authentication → Get started.
- Pick Email/Password, switch the first toggle on, Save.
Create the database
- Build → Firestore Database → Create database.
- Location: pick one near you (for the East Coast, nam5 or us-east1). Next.
- Choose Start in production mode → Create.
Paste the security rules
These make sure only you can manage the lot, and estimators only touch their own checklists.
- In Firestore, open the Rules tab and delete everything in the box.
- Tap Copy rules below, paste into the box, then tap Publish.
rules_version = '2';
// Estimate Lot: who can see and change what.
// - The manager (whoever set the app up first) controls everything.
// - Estimators must be approved by the manager. They can view the lot and
// fill out checklists for cars assigned to them, which marks those cars Done.
// - Estimators can add damage photos; the manager reviews finished checklists.
// - Nobody signed out can see anything.
service cloud.firestore {
match /databases/{database}/documents {
function signedIn() { return request.auth != null; }
function cfgPath() { return /databases/$(database)/documents/config/app; }
function isManager() { return signedIn() && exists(cfgPath()) && request.auth.uid in get(cfgPath()).data.managers; }
function mePath() { return /databases/$(database)/documents/users/$(request.auth.uid); }
function isApproved() { return signedIn() && exists(mePath()) && get(mePath()).data.approved == true; }
function member() { return isManager() || isApproved(); }
// Extra permissions the manager gives a person (users/{uid}.perms): lot, approve, stats, anycar
function approvalOff() { return get(cfgPath()).data.get('noApproval', false) == true; }
function can(p) { return isApproved() && get(mePath()).data.get('perms', {}).get(p, false) == true; }
match /config/app {
allow read: if signedIn();
// First person to finish setup becomes the manager.
allow create: if signedIn() && request.resource.data.managers == [request.auth.uid];
allow update: if isManager();
}
match /users/{uid} {
allow read: if signedIn();
allow create: if signedIn() && request.auth.uid == uid
&& (request.resource.data.approved == false || isManager());
allow update: if isManager()
|| (request.auth.uid == uid && !request.resource.data.diff(resource.data).affectedKeys().hasAny(['approved', 'perms', 'features', 'bay']));
allow delete: if isManager();
}
function bayHas(id, stock) {
return existsAfter(/databases/$(database)/documents/spots/$(id))
&& getAfter(/databases/$(database)/documents/spots/$(id)).data.stock == stock;
}
match /spots/{stall} {
allow read: if member();
// Estimators can move their own car into an open shop bay when they start its checklist
// (only into an empty bay, and only by taking it out of its old stall in the same step).
allow create: if isManager() || can('lot')
|| (isApproved() && stall in ['BAY1', 'BAY2'] && (request.resource.data.assigneeUid == request.auth.uid || can('anycar')));
allow delete: if isManager() || can('lot')
|| (can('approve') && resource.data.status == 'done')
|| (isApproved() && approvalOff() && resource.data.assigneeUid == request.auth.uid && resource.data.status == 'done')
|| (isApproved() && (resource.data.assigneeUid == request.auth.uid || can('anycar'))
&& (bayHas('BAY1', resource.data.stock) || bayHas('BAY2', resource.data.stock)));
allow update: if isManager() || can('lot')
|| (can('approve') && request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['review', 'reviewNote', 'status', 'checklistDone', 'doneAt', 'updated']))
|| (isApproved() && (resource.data.assigneeUid == request.auth.uid || can('anycar'))
&& request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['status', 'checklistId', 'checklistCount', 'checklistDone', 'updated', 'startedAt', 'doneAt']));
}
match /checklists/{id} {
allow read: if member();
allow create: if isManager()
|| (isApproved() && (request.resource.data.estimatorUid == request.auth.uid || can('anycar')));
// Only managers and people allowed to approve can change the review or the date the car left
allow update: if isManager() || can('approve')
|| (isApproved() && (request.resource.data.estimatorUid == request.auth.uid || can('anycar'))
&& !request.resource.data.diff(resource.data).affectedKeys().hasAny(['review'])
&& (approvalOff() || !request.resource.data.diff(resource.data).affectedKeys().hasAny(['leftAt'])));
allow delete: if isManager();
}
match /photos/{id} {
allow read: if member();
allow create: if member() && request.resource.data.uid == request.auth.uid
&& request.resource.data.data is string && request.resource.data.data.size() < 1000000;
allow delete: if isManager() || can('approve') || (signedIn() && resource.data.uid == request.auth.uid);
// naming a photo: only the name can change
allow update: if (isManager() || can('approve') || (signedIn() && resource.data.uid == request.auth.uid))
&& request.resource.data.diff(resource.data).affectedKeys().hasOnly(['name'])
&& request.resource.data.name is string && request.resource.data.name.size() <= 80;
}
match /log/{entry} {
allow read: if member();
allow create: if member();
allow update, delete: if isManager();
}
// Phone notifications: each phone saves its own sign-up; the notification server
// reads them (with the sender's sign-in) and records each alert once so it isn't sent twice.
match /pushSubs/{id} {
allow read: if member();
allow create, update: if member() && request.resource.data.uid == request.auth.uid
&& request.resource.data.sub is string && request.resource.data.sub.size() < 2000;
allow delete: if member();
}
match /pushEvents/{id} {
allow create: if member();
}
}
}
Get your app's connection code
- Tap the gear ⚙ next to Project Overview → Project settings.
- Scroll to Your apps, tap the web icon </>, name it Estimate Lot, tap Register app (skip Hosting).
- Copy the whole firebaseConfig block (from
{to}).
Send that block to Claude and you'll get back a version with it built in, so nobody else ever has to paste it. Or paste it on the app's setup screen to start right away.
Allow your website address
- Back in Authentication → Settings → Authorized domains → Add domain.
- Enter your website address without https:// (for example estimate-lot.pages.dev) and add it. If you also have your own domain, add that too.
Your address is:
Sign in as the manager
- Open the app, tap New here? Create an account, and use your name, email and a password.
- Tap I'm the manager. You're set.
- Have each estimator open the same address on their phone and create an account. Approve them in the Estimators panel.
Phone notifications (optional)
Lets alerts reach phones even when the app is closed or the phone is locked. Do this once the app is working. About 5 minutes.
- Paste the rules from step 4 again and Publish (they now include notifications).
- Tap Copy server code below. It makes a private key just for you, so do this on the phone or computer you'll use for the next steps.
- In Cloudflare: Workers & Pages → Create → Create Worker (Start with Hello World). Name it estlot-push, tap Deploy.
- Tap Edit code. Select everything in the code box and delete it, paste the code you copied, then tap Deploy.
- Copy that worker's link at the top (it ends in .workers.dev).
- In Estimate Lot: My alerts → paste it into Notification server → Save, then Turn on.
- Everyone else: My alerts → Turn on. On iPhone the app has to be opened from the Home Screen icon first.
Note writer (optional)
Adds a ✨ Make it professional button under notes: estimators say it plainly and it's rewritten in estimate wording. It uses Cloudflare's free AI; nothing else to pay for. About 5 minutes.
- Tap Copy note writer code below.
- In Cloudflare: Workers & Pages → Create → Create Worker. Name it estlot-ai, tap Deploy.
- Tap Edit code, delete everything in the box, paste, then Deploy.
- Go back to the worker's page → Settings → Bindings → Add → Workers AI. Variable name: AI. Save / Deploy.
- Copy the worker's link (ends in .workers.dev). In Estimate Lot: My alerts → paste it into Note writer (AI) → Save.
Put it on phones and the computer
- iPhone: open the address in Safari → Share → Add to Home Screen.
- Android: open it in Chrome → ⋮ menu → Add to Home screen / Install app.
- Windows: open it in Edge → ⋯ menu → Apps → Install this site as an app. In the app, Open display window gives you the wall view; drag it to the second monitor and press F11.