Set up your private workspace
Prime Practice is a privacy-first practice management platform built exclusively for Indian Chartered Accountants. Your client data lives in your own Firebase project, not ours.
This wizard will guide you through 5 simple steps to create your workspace. Estimated time: 15–20 minutes (mostly waiting for Firebase to provision).
What you'll do:
- Create a free Firebase project on your Google account
- Paste the SDK config into this wizard
- Enable Firestore + paste our security rules
- Enable Authentication + add authorized domains
- Pick your firm's URL slug and submit for approval
Note: All progress is saved as you go. You can close this page and resume later — just visit primepractice.in/setup-wizard again.
Create your free Firebase project
Firebase will host your workspace's database, authentication, and storage. The free tier covers all small & mid-size CA firms comfortably.
- Open console.firebase.google.com in a new tab and sign in with your Google account.
- Click "Add project" (or the giant card at center).
- Enter a project name (e.g., your firm's name). Firebase auto-generates a Project ID — you can edit it for cleanliness if you want.
- Disable Google Analytics when prompted — not needed for the workspace.
- Click Create project and wait ~30 seconds for provisioning.
Firebase's free Spark plan covers everything Prime Practice needs — Firestore (1 GB storage, 50,000 reads/day, 20,000 writes/day), Authentication (unlimited Email/Password users), and Hosting. For a typical small or mid-size CA firm, you will never exceed the free limits.
Do NOT upgrade to Blaze (the paid plan) unless you specifically want premium features in the future. If Firebase asks "Upgrade to Blaze?" anywhere during setup, just say No / close the dialog.
Done? Click Next below — we'll set up the web app + grab the SDK config in the next step.
Connect your Firebase project
Inside Firebase Console, register a Web app and copy its SDK config snippet. Paste it below — we'll validate it works.
- In your Firebase project dashboard, click the </> (Web) icon under "Get started by adding Firebase to your app".
- App nickname:
Prime Practice MT(or anything). Leave "Also set up Firebase Hosting" unchecked. - Click Register app.
- Firebase shows a
const firebaseConfig = { ... }snippet. Copy the entire block (all 6 fields). - Paste it into the textarea below ↓ — the 6 fields auto-fill.
Enable Firestore + apply our security rules
Firestore is the database that stores your clients, tasks, and compliance data. The security rules below ensure only you and your invited staff can access it.
We'll enable the required APIs, deploy security rules, configure Authentication, and add authorized domains for you. One-time access — token is used once and discarded. Saves you ~10 manual clicks.
Full transparency — here's exactly what happens
- Create your Firestore database in asia-south1 (if not already done)
- Deploy our recommended security rules (shown below)
- Enable Email/Password + Google sign-in
- Add
primepractice.into your authorized domains
- Store your Google credentials anywhere on our servers
- Read your Firestore data (zero data access)
- Access any other Google service (Drive, Gmail, etc.)
- Use the token a second time — it's discarded immediately after setup
firebase (manage Firebase resources) + service.management (enable required APIs on your project). Both are Firebase/GCP project-management scopes only — we cannot read your Firestore data, Gmail, Drive, or any other Google service.
- In your Firebase project, go to Build → Firestore Database.
- Click Create database. Location:
asia-south1 (Mumbai). Mode:Production mode. Click Create. - After provisioning (~30 sec), switch to the Rules tab.
- Click the "Copy rules" button below, then paste in the Rules editor (replace everything) and click Publish.
rules_version = '2';
// ═══════════════════════════════════════════════════════════════════
// PRIME PRACTICE — FIRESTORE SECURITY RULES (v5 — granular S4)
// ═══════════════════════════════════════════════════════════════════
// Deploy:
// Firebase Console → Firestore Database → Rules → Paste → Publish
// or:
// firebase deploy --only firestore:rules
// ═══════════════════════════════════════════════════════════════════
service cloud.firestore {
match /databases/{database}/documents {
// ── Helper functions ─────────────────────────────────────────
function isSignedIn() { return request.auth != null; }
function isAdmin(firmId) { return isSignedIn() && request.auth.uid == firmId; }
function isStaff() {
return isSignedIn()
&& request.auth.token.email != null
&& request.auth.token.email.matches('.*@staff[.]local');
}
function staffUsername() { return request.auth.token.email.split('@')[0]; }
function staffDoc() {
return get(/databases/$(database)/documents/staffIndex/$(staffUsername())).data;
}
function isStaffOfFirm(firmId) {
return isStaff()
&& exists(/databases/$(database)/documents/staffIndex/$(staffUsername()))
&& staffDoc().firmId == firmId;
}
function staffHasPerm(firmId, perm) {
return isStaffOfFirm(firmId)
&& staffDoc().permissions != null
&& staffDoc().permissions[perm] == true;
}
function isMemberOfFirm(firmId) { return isAdmin(firmId) || isStaffOfFirm(firmId); }
// ── staffIndex (top-level) ───────────────────────────────────
// v27 redesign: key is now mobile number (was username). Logic
// unchanged because the key just needs to match the staff's
// Firebase Auth email prefix ({key}@staff.local).
//
// FIX (Phase 8 Issue P3-3): hijack prevention. An admin can only
// CREATE a new staffIndex doc, OR UPDATE one that already belongs
// to their firm. Cannot overwrite another firm's staffIndex entry.
match /staffIndex/{username} {
// FIX (Phase 17): Allow unauthenticated READ. Required because staff
// mobile login must look up loginEmail BEFORE Firebase Auth sign-in.
// Beta trade-off (similar to staffIndex_email):
// - Data exposed: name, mobile, firmId, permissions, loginEmail
// - Mitigation: in beta, only known users have the app URL
// - Public launch: replace with Cloud Function callable that does
// the lookup server-side (eliminating the open read).
allow read: if true;
// CREATE: admin (uid==firmId) OR a staff with `staffSetup` permission.
// The new doc must declare the SAME firmId — no hijacking other firms.
allow create: if isSignedIn()
&& resource == null
&& request.auth.uid == request.resource.data.firmId;
allow create: if resource == null
&& request.resource.data.firmId is string
&& isStaffOfFirm(request.resource.data.firmId)
&& staffDoc().permissions.staffSetup == true;
// UPDATE: admin OR a `staffSetup` staff of the SAME firm.
allow update: if isSignedIn()
&& resource != null
&& request.auth.uid == resource.data.firmId
&& request.auth.uid == request.resource.data.firmId;
allow update: if resource != null
&& resource.data.firmId == request.resource.data.firmId
&& isStaffOfFirm(resource.data.firmId)
&& staffDoc().permissions.staffSetup == true;
// DELETE: admin OR a `staffSetup` staff of the SAME firm.
allow delete: if isSignedIn() && resource != null && request.auth.uid == resource.data.firmId;
allow delete: if resource != null
&& isStaffOfFirm(resource.data.firmId)
&& staffDoc().permissions.staffSetup == true;
}
// ── staffIndex_email (top-level lookup pointer) ──────────────
// v27: Maps staff email → {mobile, firmId} so staff can also log in
// using their email instead of mobile number.
//
// BETA TRADE-OFF: read is open to anyone (`if true`) because the
// lookup must happen BEFORE login (to translate email → mobile
// before signInWithEmailAndPassword). Acceptable for beta because:
// - Data is minimal (just mobile + firmId for that email)
// - Email enumeration is already possible via Firebase Auth itself
// - Closed beta with 30-40 known users
//
// PUBLIC LAUNCH: Replace with a Cloud Function callable that does
// the lookup server-side, removing this open read.
//
// v2 2026-05-25: writes also allowed for staff with `staffSetup` perm.
match /staffIndex_email/{email} {
allow read: if true;
allow create, update: if isSignedIn() && request.auth.uid == request.resource.data.firmId;
allow create, update: if request.resource.data.firmId is string
&& isStaffOfFirm(request.resource.data.firmId)
&& staffDoc().permissions.staffSetup == true;
allow delete: if isSignedIn() && resource != null && request.auth.uid == resource.data.firmId;
allow delete: if resource != null
&& isStaffOfFirm(resource.data.firmId)
&& staffDoc().permissions.staffSetup == true;
}
// ── firms/{firmId} ───────────────────────────────────────────
match /firms/{firmId} {
allow read: if isMemberOfFirm(firmId);
allow create: if isSignedIn() && request.auth.uid == firmId;
allow update, delete: if isAdmin(firmId);
// ── clients (v2 2026-05-25: canEdit / canDelete sub-permissions) ──
// FIX (Phase 3 audit Issue 1): use `!= true` instead of `== false`
// so clients without the hideFromStaff field (legacy / direct console
// edits / restored backups) are NOT silently hidden from staff.
//
// v2 model (2026-05-25): module-access PLUS sub-permission for writes.
// - `clients` perm → read (gated by hideFromStaff)
// - `clients` + canEdit → create / update
// - `clients` + canDelete → delete
match /clients/{clientId} {
allow read: if isAdmin(firmId);
allow read: if isStaffOfFirm(firmId) && resource.data.hideFromStaff != true;
allow create, update: if isAdmin(firmId);
allow create, update: if staffHasPerm(firmId, 'clients')
&& staffDoc().permissions.canEdit == true;
allow delete: if isAdmin(firmId);
allow delete: if staffHasPerm(firmId, 'clients')
&& staffDoc().permissions.canDelete == true;
}
// ── tasks (v3 2026-05-25 evening: peer-to-peer reassignment) ──
// Existing assigned-task update path preserved (any staff can update
// their own task; staffName field still locked from reassignment
// UNLESS they're acting on a pending reassignment request to them).
// NEW v2: a staff with `bulkOps` perm can create + delete tasks.
// NEW v3: target staff can accept (changes staffName to them) or
// reject (keeps staffName), both clear pendingReassignment.
match /tasks/{taskId} {
allow read: if isMemberOfFirm(firmId);
allow create, delete: if isAdmin(firmId);
allow create, delete: if staffHasPerm(firmId, 'bulkOps');
// Admin: full update access
allow update: if isAdmin(firmId);
// Staff: update only if task is assigned to them
// (compares stored staffName with their staffIndex.name)
// FIX (Phase 3 audit Issue 2): also lock staffName field so staff
// cannot reassign their own task to another staff member.
allow update: if isStaffOfFirm(firmId)
&& resource.data.staffName == staffDoc().name
&& request.resource.data.staffName == resource.data.staffName;
// bulkOps staff can also update tasks they aren't assigned to
// (used by bulk operations like mass-status-update).
allow update: if staffHasPerm(firmId, 'bulkOps');
// v3 PEER REASSIGN — target staff can ACCEPT a pending request:
// - staffName changes to them
// - pendingReassignment field is cleared (set to null)
allow update: if isStaffOfFirm(firmId)
&& resource.data.pendingReassignment != null
&& resource.data.pendingReassignment.toStaff == staffDoc().name
&& request.resource.data.staffName == staffDoc().name
&& request.resource.data.pendingReassignment == null;
// v3 PEER REASSIGN — target staff can REJECT a pending request:
// - staffName stays unchanged
// - pendingReassignment field is cleared (set to null)
allow update: if isStaffOfFirm(firmId)
&& resource.data.pendingReassignment != null
&& resource.data.pendingReassignment.toStaff == staffDoc().name
&& request.resource.data.staffName == resource.data.staffName
&& request.resource.data.pendingReassignment == null;
// Task chat — both can read & post; staff can only post if task is theirs
match /chat/{messageId} {
allow read: if isMemberOfFirm(firmId);
allow create: if isAdmin(firmId);
allow create: if isStaffOfFirm(firmId)
&& get(/databases/$(database)/documents/firms/$(firmId)/tasks/$(taskId)).data.staffName == staffDoc().name;
allow update, delete: if isAdmin(firmId);
}
}
// ── transactions (v2 2026-05-25: finance perm grants full CRUD) ──
match /transactions/{txId} {
allow read: if isAdmin(firmId);
allow read: if staffHasPerm(firmId, 'finance');
allow write: if isAdmin(firmId);
allow write: if staffHasPerm(firmId, 'finance');
}
// ── expenses (v5 2026-05-25: Out-of-Pocket expenses) ──
// Tracks client-related expenses the firm incurs on the client's
// behalf (stamp paper, notary, gov fees, courier). Same access model
// as transactions: admin OR staff with finance perm.
match /expenses/{expenseId} {
allow read: if isAdmin(firmId);
allow read: if staffHasPerm(firmId, 'finance');
allow write: if isAdmin(firmId);
allow write: if staffHasPerm(firmId, 'finance');
}
// ── staff records (v2 2026-05-25: staffSetup perm grants full CRUD) ──
// Self-read preserved for any signed-in staff (they need their own
// record for visibility flags etc.). Writes now allowed for staff
// with `staffSetup` — they can create / edit / delete other staff.
match /staff/{staffId} {
allow read: if isAdmin(firmId);
allow read: if isStaffOfFirm(firmId)
&& resource.data.username == staffUsername();
allow read: if staffHasPerm(firmId, 'staffSetup');
allow write: if isAdmin(firmId);
allow write: if staffHasPerm(firmId, 'staffSetup');
}
// ── groups (v2 2026-05-25: clients+canEdit / canDelete) ──
// Groups are an extension of Client Master (contact-info grouping).
// Writes follow the same sub-permission model as clients.
match /groups/{groupId} {
allow read: if isAdmin(firmId);
allow read: if staffHasPerm(firmId, 'clients');
allow create, update: if isAdmin(firmId);
allow create, update: if staffHasPerm(firmId, 'clients')
&& staffDoc().permissions.canEdit == true;
allow delete: if isAdmin(firmId);
allow delete: if staffHasPerm(firmId, 'clients')
&& staffDoc().permissions.canDelete == true;
}
// ── compliance master (v2 2026-05-25: compliance perm grants full CRUD) ──
match /compliances/{compId} {
allow read: if isAdmin(firmId);
allow read: if staffHasPerm(firmId, 'compliance') || isStaffOfFirm(firmId);
// ↑ Staff still needs read access for task badges.
allow write: if isAdmin(firmId);
allow write: if staffHasPerm(firmId, 'compliance');
}
// ── notifications (v3 2026-05-25: staff can create reassign pings) ──
match /notifications/{notifId} {
allow read: if isMemberOfFirm(firmId);
allow create, delete: if isAdmin(firmId);
// v3 NEW: any staff member of the firm can create notifications
// (needed for reassign_request / reassign_accepted / reassign_rejected
// /reassign_cancelled / reassign_expired pings). Tightened to require
// that the notification's fromStaff matches the actor — prevents
// staff from posting in another staff's name.
allow create: if isStaffOfFirm(firmId)
&& request.resource.data.fromStaff == staffDoc().name;
// Staff can delete their own outgoing notifications (e.g. cancel
// a pending reassign request).
allow delete: if isStaffOfFirm(firmId)
&& resource.data.fromStaff == staffDoc().name;
// Update: admin OR target staff (mark as read)
allow update: if isAdmin(firmId);
allow update: if isStaffOfFirm(firmId)
&& resource.data.targetStaff == staffDoc().name;
}
// ── app updates / changelog ──
match /appUpdates/{docId} {
allow read: if isMemberOfFirm(firmId);
allow write: if isAdmin(firmId);
}
// ── WhatsApp templates ──
match /waTemplates/{templateId} {
allow read: if isMemberOfFirm(firmId);
allow write: if isAdmin(firmId);
}
// ── Email templates ──
match /emailTemplates/{templateId} {
allow read: if isMemberOfFirm(firmId);
allow write: if isAdmin(firmId);
}
// ── auditLog (append-only trail) ──
// FIX (Phase 3 audit Issue 4): audit logs are now FULLY immutable.
// Even admin cannot delete — required for compliance / tamper-proof
// trail. To purge old logs, use a Cloud Function with admin SDK
// (which bypasses these rules) on a retention schedule.
match /auditLog/{logId} {
allow read: if isAdmin(firmId);
allow create: if isMemberOfFirm(firmId);
allow update: if false;
allow delete: if false;
}
// ── recurringSchedules (v23 — admin-only, no staff visibility) ──
match /recurringSchedules/{scheduleId} {
allow read, write: if isAdmin(firmId);
}
// Catch-all (admin-only) for any future subcollection
match /{document=**} {
allow read, write: if isAdmin(firmId);
}
}
}
}
Enable Authentication + add authorized domains
This lets your firm's admin and staff sign in. We'll enable two sign-in methods.
- In Firebase Console → Build → Authentication. Click Get started.
- Under the Sign-in method tab, click Email/Password → toggle the first switch ON → Save. (You do not need to enable Google or any other provider — Prime Practice uses only Email/Password.)
- Switch to the Settings tab → Authorized domains section.
- Click Add domain and add both of these (one at a time):
primepractice.inmt.primepractice.in
primepractice.in means your staff can log in to primepractice.in/your-firm.
Pick your URL + submit for approval
Choose the URL slug your firm will use. Submit, and we'll review your setup within 24 hours.
rajesh-co, shantanu.After you submit, your entry will be marked pending. Our team reviews each setup to ensure your Firebase project works correctly. Once approved, you'll receive an email and your workspace will go live at primepractice.in/{slug}.
Submission received!
Your firm admin account has been created in your Firebase project, and your workspace is now in our review queue. We'll email you within 24 hours when activated.
You can already test the URL before approval — the page should load but login will be blocked until our team finishes the security check.