Skip to main content

Quotas, limits, fair-use thresholds

The app enforces a set of numeric ceilings. Some exist because of platform constraints (Firestore document size, Android carrier behaviour); others exist because of explicit product policy (fair-use across volunteers, anti-abuse). This page consolidates every number in one place so support, ops, and curious users can read the limits without having to reverse-engineer them from the codebase.

Each row notes where the limit is enforced — server-side rules, client-side service module, or remote sms_config — so it is clear whether a value is movable per environment or hard-coded.

Pre-login guest allowance

LimitValueEnforced where
Free SMS before sign-in10 per opaque guestIdfirestore.rules on sms_guest_quota (count ≤ 10, monotonic non-decreasing) + client mirror in Capacitor Preferences.
Guest record retentionIndefinite (linked to UID on first login)Client + rules; admin can delete for fraud cleanup.
Anonymous write surfaceOnly count, guestId, createdAt, updatedAt may be set anonymouslyfirestore.rules (the isGuestWriteShape helper).
LinkingOnly an authenticated user can stamp linkedUid, linkedAppIdentifier, linkedAt; the count must not change on the link writefirestore.rules (the isLinkWriteShape helper).

The 10-SMS allowance is per guestId, not per device. The guestId is generated client-side and stored in Capacitor Preferences, so reinstalling the app or clearing storage produces a fresh guestId — but in practice we observe near-zero abuse because the in-app composer requires the user to verify their own number for the first send.

Per-message limits

LimitValueEnforced where
Phone number length7–20 charsfirestore.rules isValidPhone().
Message body length1–500 charsfirestore.rules isValidMessage().
GSM-7 single-segment160 charsReported by smsSegments.ts in the composer; not a hard cap.
UCS-2 single-segment70 charsSame — composer warns when a non-GSM-7 character bumps the segment count.
Max segments per message6 segmentsComposer warning at 4, hard stop at 6.

The 500-char rule cap is a sanity bound. Carriers typically charge per 160-char (GSM-7) or 70-char (UCS-2) segment regardless of what we cap at. The composer surfaces the segment count and estimated carrier-charge breakdown in real time.

Job & retry limits

LimitValueEnforced where
attemptCount on creationMust be 0firestore.rules isValidJobCreate().
status on creationMust be 'pending'firestore.rules isValidJobCreate().
Default maxAttempts per recipient3BatchSendRunner.java + recipientReclaimService.ts. Configurable per-batch via the admin dashboard.
Backoff scheduleExponential with jitter: 30s × 2^retryCount + random(0..15s)smsAutomation/ retry helper.
Permanent-failure no-retry codesINVALID_NUMBER, BLOCKED_BY_USER, CARRIER_REJECT_PERMANENTClient classifier in the runner.
Bounded attempt history5 entries max per recipientfirestore.rules on sms_batches/{id}/recipients/{rid} (attemptHistory.size() ≤ 5).
Job-claim expiry5 minutes without progressrecipientReclaimService.ts sweeps and releases.

Device & batch limits

LimitValueEnforced where
Max devices per batch (hard cap)10firestore.rules on sms_batches (every create + update).
Max batches per admin per day (default)5 (configurable via sms_config)Client + admin dashboard.
Default per-device rate cap6 SMS/minuteVolunteer-chosen at registration; runner enforces in-process.
Maximum per-device rate cap30 SMS/minuteHard cap in the runner — even if a user requests higher, the runner clamps.
Per-account aggregate ceiling200 SMS/hour by default; higher via fair-use reviewsms_config (remote).
Device-rename cooldown7 days between renamesfirestore.rules on sms_devices (request.time ≥ deviceNameLastEditedAt + duration.value(7,'d')).
Auto-flag threshold (device health)5 consecutive failures → status = 'flagged'Client + runner; rules enforce that only 'active''flagged' can be toggled by non-admin.
Auto-flag clear durationConfigured per environment (default 24h)Stored in sms_devices.flagAutoClearAt.
Flagged-device fan-out gateNo batch can fan out to a flagged devicefirestore.rules on sms_batches (areAllAssignedDevicesHealthy()).
recentSendOutcomes auditBounded to ~50 entries client-sidesmsDevicesService.ts.

The 10-device cap is the single most-asked-about ceiling. The full rationale is in Enforce the 10-device cap. Briefly: it prevents single-account monopolisation of the volunteer pool, limits carrier-side anti-spam exposure, and keeps the Firestore rules-side iteration tractable (rules cannot loop arbitrarily — the 10-element walk is hand-unrolled).

Batch size limits

LimitValueEnforced where
Recipients per batchUp to 5,000 (soft, configurable via sms_config)CSV import client + admin dashboard.
Recipients per batch (hard ceiling)10,000Server-side composite-write budget; beyond this, batch creation is rejected to protect Firestore write quotas.
Max sub-collection write rate~500 writes/sec per batchFirestore platform limit — the runner is paced well below this.
CSV file size5 MBClient upload cap (FilesHub-enforced).
Contact-form message length5,000 charsfirestore.rules isValidContactSubmissionCreate().

If a single workload would exceed 5,000 recipients, the admin dashboard splits it into multiple sequential batches (Campaign chunking). The user UX surfaces it as a Campaign card composed of N batches in serial.

Form-input limits

LimitValueEnforced where
Contact name1–100 charsfirestore.rules.
Contact email1–200 charsfirestore.rules.
Contact subject1–200 charsfirestore.rules.
Contact message1–5,000 charsfirestore.rules.
Template name1–120 charsClient schema.
Template body1–500 charsClient schema (mirrors message limit).
Draft body1–500 charsClient schema.
Group name1–80 charsClient schema.

Fair-use thresholds

Fair-use is the soft policy layer that sits above hard rules-enforced limits. Crossing a fair-use line does not auto-reject — it triggers a review.

ThresholdValueTrigger
Per-account daily volume (free tier)2,000 sends/dayAuto-paused; admin notified; user emailed with appeal instructions.
Per-account weekly volume10,000 sends/weekSame.
Recipient-uniqueness ratio< 50% unique recipients per batchFlagged for review — high duplication is a spam signal.
Failure ratio (sustained)> 30% failures over 100+ attemptsPer-device auto-flag (covered above) + account flag in extreme cases.
Carrier-throttle ratio> 20% CARRIER_THROTTLED errors per batchAuto-backoff kicks in; the runner slows by 50% for the rest of the batch.
Upgrade pathtier: 'fair_use_upgraded'Admin-managed via /admin/users. Raises daily/weekly caps.

Detailed fair-use policy is at /fair-use on the main app; the How to configure rate limits recipe covers the operational levers.

Session limits

LimitValueEnforced where
ID-token refreshEvery 45 minutes in backgroundAuthProvider.
Session restore on cold startYes (Firebase Auth persistence)Default.
EnvMissingScreen blockerRenders before any feature mounts if a required env is missingsrc/config/env.ts.

Where these numbers come from

Every numeric limit traces back to one of three sources:

  1. firestore.rules — the canonical, server-enforced ceilings (10-device cap, 500-char messages, attempt-history size, guest 10-SMS, etc.).
  2. sms_config Firestore collection — admin-tunable remote configuration. /admin/config is the dashboard for these.
  3. Client constants in the relevant service module — e.g. recipientReclaimService.ts for claim-expiry, smsAutomation/ for backoff schedule.

When in doubt about a specific value in a deployment, the diagnostic at /admin/diagnostics prints the effective values for that environment.