Privacy Policy
Effective from: [FOUNDER FILL — YYYY-MM-DD]
Last updated: [FOUNDER FILL — YYYY-MM-DD]
Version: 1
This Privacy Policy explains how PlanooAI ("we", "us", "the platform") collects, uses, retains, and protects personal data. It is grounded in the actual code that runs the product — every claim below can be verified against a source file cited in line. Nothing here is aspirational. If we do not do something today, we do not promise it.
1. Who we are
- Controller: [FOUNDER FILL — registered legal entity name]
- Commercial Registration (CR) number: [FOUNDER FILL]
- Registered HQ address: [FOUNDER FILL]
- Data Protection Officer: [FOUNDER FILL — name + contact, OR
"no DPO appointed; below the PDPL volume threshold"]
- Primary jurisdiction: Kingdom of Saudi Arabia.
- Regulated by: the Saudi Data & Artificial Intelligence Authority
(SDAIA) under the Personal Data Protection Law (PDPL).
2. What we collect
The data we collect is what the product needs to function. Each class below maps to specific tables and fields in the code.
2.1 Account data
- Email address, name, role, preferred language (
ar/en), optional
phone number, and password (bcrypt-hashed; never stored in plaintext) — see apps/api/models.py:32 (User class) and the hashing routine at apps/api/auth.py:66 (hash_password).
- Account-lockout state (
failed_login_count,locked_until) used
only to throttle brute-force attempts.
2.2 Organization data
- Organization name, sector, country, headcount, plus the strategic
identity fields entered through the planning wizard (vision, mission, values, target segments, strategic priorities, etc.) — see apps/api/models.py:84 (Organization class).
- The wizard never collects per-person financial data; revenue, when
present, is banded.
2.3 Authentication data
- Session and refresh tokens delivered as **HttpOnly, Secure,
SameSite** cookies (planoo_access, planoo_refresh) so a browser XSS cannot read them — see apps/api/token_utils.py:64 (set_auth_cookies).
- MFA codes are HMAC-SHA256 hashed before storage; the plaintext
code never persists — see apps/api/token_utils.py:43 (hash_code) and the constant-time comparison at apps/api/auth.py:1331.
- Refresh tokens rotate on every refresh, with replay detection: a
token presented after a 30-second grace window revokes the entire family — see apps/api/auth.py:1939 (refresh handler).
2.4 Operational data
- KPI values, baselines, targets, project status, ownership
assignments, audit log entries.
- Audit log rows record the *event* (e.g.
ai.generation,
legal.privacy_updated) plus organization and user identifiers; no message bodies, no request payloads — see apps/api/models.py:559 (AuditLog class).
2.5 Payment data
- Moyasar checkout intent metadata (gateway reference, amount,
currency, status) — see apps/api/billing.py:409 (POST /v1/billing/checkout-intents).
- **Card numbers (PANs), CVCs, and expiry dates never touch our
infrastructure.** The Moyasar hosted form collects them directly from the customer and returns only a tokenised reference. We then verify the payment status via the gateway-signed webhook at apps/api/billing.py:620 (POST /v1/billing/webhooks/moyasar), which is the only authority for billing state changes.
2.6 Communications data
- One-time codes are delivered by SMS (Taqnyat / Unifonic / Twilio,
depending on operator selection — see KNOWN_SMS_ENDPOINTS at apps/api/integrations.py:299) or by email (SMTP relay configured per environment).
- The body of OTP, password-reset, and similar messages is **never
logged**; the logging path emits only environment, channel, and destination, with the code explicitly withheld — see apps/api/otp_delivery.py:38 (send_otp_code).
2.7 Telemetry data
- We capture request and span metadata (path, status code, duration)
for observability and tracing — see apps/api/observability.py:1 and apps/api/tracing.py:1.
- We never opt into body capture or header capture in OpenTelemetry
instrumentation, so no auth headers, cookies, or request bodies enter spans — see the explicit note at apps/api/tracing.py:23.
- Log lines pass through a redactor that strips
password,secret,
token, and authorization substrings before any line is written.
3. What we send to AI providers
The AI generation chain (SWOT, PESTEL, objectives, KPIs, recommendations) calls a third-party LLM. We constrain what reaches the provider by an explicit allow-list in code.
3.1 What CAN leave our infrastructure
- Allow-listed organization profile fields (
SafeOrganizationProfile
at apps/api/ai_models.py:156): - *Tier 1 — strategic identity:* vision, mission, values, business_model, target_segments, strategic_priorities. - *Tier 2 — structural facts:* sector, sub_sector, country (ISO-2), headcount_band, founding_year_band, revenue_band, markets_served. - *Tier 3 — operational nuance:* competitive_positioning, distribution_channels, regulatory_environment, has_vision_mission.
- Aggregated organization context — department names with
*headcounts only* (no per-person data), prior-cycle KR attainment bands, plan-structure counts (# directions, # objectives, # KRs). Source: apps/api/ai_endpoints.py:_get_organization_context.
- Regional context pack — public information about KSA / GCC
operating environment (Vision 2030 programs, ZATCA, PDPL itself, SAMA licensing, etc.). This is not customer data.
3.2 What is BLOCKED from reaching the provider
- Per-person names, emails, phone numbers, user-ids.
- Customer lists, partner lists, distribution lists.
legal_nameof the organization — deliberately excluded so the LLM
cannot pull external knowledge (news, lawsuits) about the named company.
- Free-form
summary,customer_reach,expansion_planned,
other_considerations, target_age, target_gender fields.
The block is enforced in two layers:
- *Input scrub:*
SafeOrganizationProfileisextra="ignore"; any
field outside the allow-list is dropped before the prompt is rendered. Email- and phone-shaped substrings in allowed fields are rejected at the schema layer (HTTP 422).
- *Output scrub:* responses from the LLM pass through
scrub_output
at apps/api/ai_runtime.py:scrub_output before persistence. Email, phone, and proper-noun denylist patterns are redacted.
The PII contract has a CI regression guard: apps/api/tests/test_ai_pii_redaction.py.
3.3 Which provider is in use
The active provider is operator-configurable via Platform Settings. Today's defaults per environment:
| Environment | Default provider | Region |
|---|---|---|
| Development / Local | OpenAI or Gemini (operator choice) | US (OpenAI) / EU (Gemini) |
| Staging | OpenAI | US |
| Production | [FOUNDER FILL — OpenAI / Gemini / HUMAIN] | per provider |
AIProvider.HUMAIN (Saudi state-owned sovereign LLM, hosted in KSA) is configured as a procurement slot at apps/api/ai_runtime.py and will be the default once its public API contract stabilises. When HUMAIN is selected, the cross-border transfer disclosure in §6 narrows to
no cross-border transfer.
4. Legal bases for processing (PDPL Article 5)
| Activity | Legal basis |
|---|---|
| Creating and operating your account, executing your strategic plan | Performance of contract |
| Issuing ZATCA-compliant invoices, retaining audit logs for regulator inspection | Legal obligation |
| Detecting brute-force login attempts, rate-limiting abusive traffic, refusing SSRF-shaped webhook destinations | Legitimate interest |
| Sending data to an AI provider outside KSA when one is selected (§6) | Explicit consent, captured at signup via the consent checkbox on RegisterPage and AcceptInvitePage |
5. Sub-processors
We rely on a small set of named sub-processors. The current list is maintained in code: see Sub-processors for the authoritative table (provider, purpose, data shared, region, source in code). A change to that list is a documentation update, not a re-version of this Privacy Policy.
6. Cross-border transfers (PDPL Article 29)
When the operator selects a non-KSA AI provider (OpenAI in the US, Gemini in the US/EU), sending allow-listed organization context to that provider constitutes a cross-border transfer. We rely on your explicit consent at signup (PDPL Article 29(2)(d)) as the lawful basis for this transfer. The consent checkbox on the registration form links to this Privacy Policy and to the Sub-processors list; submitting the form without it ticked is rejected by the server.
Other cross-border touchpoints:
- Twilio (US) for SMS OTP delivery, when SMS is configured. Phone
number + one-time code only.
- Moyasar is KSA-based; payment data does not leave KSA.
- SMTP relay, object storage, Postgres, Redis, OpenTelemetry
endpoints are operator-supplied; their region depends on the deployment.
If the operator switches the default provider to HUMAIN (in-KSA sovereign LLM), the cross-border transfer of organization context to the LLM ceases on that environment and a notice is posted in-app.
7. Retention
We retain personal data only as long as needed for the purpose it was collected for. The class-by-class schedule is fixed in code where applicable:
| Data class | Retention | Source |
|---|---|---|
| Account profile | For the lifetime of the account, then 90 days post-termination for export | §8.5 below |
| Session + refresh tokens | Until logout, manual revocation, or token TTL expiry (whichever is earliest) | apps/api/auth.py:1939 |
| Audit log entries | 3 years, then archived to cold storage | Open SEC-06 remediation; this retention is now declared and enforced |
AI job results (ai_jobs.result) | 90 days from completion, then hard-deleted | this PR (the §13 reaper extends to enforce retention) |
| Strategy plans, KPI history | Lifetime of the subscription + 90 days post-termination for export | §8.5 below |
| Backups (PG + object storage) | 30 days encrypted snapshots, off-site, checksum-verified | docs/runbooks/backup_restore.md (P0-4); self-test at scripts/backup_restore_selftest.sh |
8. Your rights (PDPL Articles 9–13)
You — the data subject — have the right to:
- Access the personal data we hold about you (Article 9).
- Rectify inaccurate data (Article 10).
- Erase your data, subject to legal-retention obligations like
ZATCA invoicing (Article 11).
- Port your data to another controller in a structured,
machine-readable format (Article 12).
- Object to processing based on legitimate interest (Article 13).
- Restrict processing while a dispute is open (Article 13).
- Withdraw consent at any time, including the cross-border AI
consent in §6; withdrawal pauses AI features until the operator selects an in-KSA provider.
- Complain to SDAIA — the regulator — if we have not resolved
your concern within 30 days.
8.5 How to exercise these rights
Email [email protected]. We respond within 30 calendar days (PDPL standard). For account-related issues use [email protected].
If your account is terminated and you do not export your data within 90 days, your strategic-plan content, KPI history, and AI job results are hard-deleted on rolling daily passes. Backups age out within 30 days thereafter.
9. Security measures
We deploy industry-standard controls. The list below is a non-exhaustive selection of what is enforced in code today:
- Secrets at rest are Fernet-encrypted with a per-deployment master
key — see apps/api/secrets_crypto.py (encrypted columns include the AI provider API key, SMTP password, SMS API key, S3 credentials).
- Cookie auth is HttpOnly + Secure + SameSite — see
apps/api/token_utils.py:64 (set_auth_cookies).
- Passwords are bcrypt-hashed with per-row salts — see
apps/api/auth.py:66.
- MFA codes are HMAC-SHA256 hashed, compared with constant-time
equality — see apps/api/token_utils.py:43 and the comparison at apps/api/auth.py:1331.
- TLS 1.2 / 1.3 at the edge with ECDHE ciphers, HSTS preloaded,
X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, COOP headers — see nginx/production.conf.
- Refresh tokens rotate on every refresh with replay detection
(_REFRESH_REUSE_GRACE_SECONDS = 30; a token reused outside the window revokes the entire family) — see apps/api/auth.py:1939.
- Uploaded files are magic-byte validated before storage; a
spoofed extension is rejected — see apps/api/storage.py:84 (_verify_magic_bytes). Allowed types per upload class are pinned (ALLOWED_REPORT_TYPES, ALLOWED_AVATAR_TYPES, ALLOWED_LOGO_TYPES).
- Outbound webhook destinations are DNS-pinned with SSRF guards;
a host that resolves to a private/loopback address is rejected at enqueue AND re-validated at delivery time — see apps/api/webhooks.py:187 (_resolve_webhook_target) and the re-resolution at delivery (apps/api/webhooks.py:439).
- PII is redacted before every LLM request by the input allow-list
+ aggregate context loader, and re-scrubbed on the way out — see §3 above and apps/api/ai_endpoints.py:_get_organization_context. CI guard: apps/api/tests/test_ai_pii_redaction.py.
- Account brute-force protection — per-account + per-IP counters
in Redis; failure-closed when Redis is unavailable.
- **Backups are encrypted, off-site, checksum-verified, and
restore-tested** by a script that fails the CI if a backup cannot be restored — see scripts/backup_restore_selftest.sh and the runbook at docs/runbooks/backup_restore.md.
This list is not exhaustive. Our security posture evolves with each release; the items above are the controls we will not regress without notice.
10. Cookies
The product is cookie-minimal. We use:
planoo_accessandplanoo_refresh— first-party authentication
cookies, HttpOnly + Secure + SameSite.
We do not set third-party tracking cookies. We do not set analytics cookies unless an operator deploys our optional OpenTelemetry collector and selects analytics tags; even then, the collector receives only non-PII metadata (path, status, duration).
A cookie consent banner is not displayed because no consent-required cookies are set.
11. Children
The service is 18+ only. Accounts are created on the basis that the registrant is at least 18 years of age and has the legal capacity to enter into a binding agreement. We do not knowingly collect personal data from minors.
12. Changes to this policy
For material changes, we will:
- Update the
Effective fromandLast updateddates above. - Bump the Version field.
- Archive the prior version under
docs/legal/archive/v{N}/. - Notify every active account by email and an in-app banner at least
30 days before the change takes effect.
- Write an
AuditLog(event_type="legal.privacy_updated")row on
deploy, capturing the new version.
Non-material changes (typos, link fixes, code-citation updates) are documented in the changelog at the bottom but do not bump the version.
13. Contact and complaints
For data-subject rights requests, security disclosures, and privacy questions:
- Email: [email protected]
- Response window: 30 calendar days.
For unresolved complaints, you may escalate to:
- Saudi Data & Artificial Intelligence Authority (SDAIA) —
the supervisory authority for PDPL in the Kingdom.
For general support: [email protected].
14. Effective date
This policy is effective from [FOUNDER FILL — YYYY-MM-DD].
*See also:*