Files
accounted/.compliance/ropa.yaml
T
88760ae6f6 fix(whatsapp-inbox): harden against adversarial review findings (#1342)
* fix(whatsapp-inbox): erase the WhatsApp channel on account deletion

whatsapp_phone_links relied on the auth.users ON DELETE CASCADE, but
Accounted never deletes auth.users: account deletion is
anonymize_user_account plus a ~100-year ban that keeps the auth row as a
tombstone, so the cascade never fires and nothing revokes the link. After
erasure the link stayed active with a decryptable phone_enc,
lookupActiveLink kept resolving the number, and every further inbound
message was persisted with body_text and the verbatim raw_payload while
the bot kept replying: GDPR Art 17 plus continued collection with no
lawful basis.

The RPC is re-created verbatim from 20260724150000 with one added block
that revokes and crypto-shreds the link, resets its conversation, nulls
body_text/raw_payload on that link's messages and deletes outstanding
link codes, plus a guarded repair pass for tombstones anonymized before
this migration. Covered by a pg-real test that fails against the previous
definition.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(whatsapp-inbox): pepper the link-code hash and bound code minting

hashLinkCode stored a bare sha256 over CODE_ALPHABET^6 = 30^6 values
behind a fixed 'AC-' prefix. The module cited the invite-token pattern,
but invite tokens are 256-bit random; this space enumerates offline in
about a second, so hashing at rest protected nothing. The sibling
phone-crypto.ts already states the team's own threat model for a LARGER
space ("a plain sha256 would be brute-forceable ... hence the pepper"),
so link codes now hash through the same env-mandated pepper.

/link/start was also an authenticated unbounded INSERT that left every
earlier code valid. Minting now burns the caller's unused codes (the code
the panel shows is the only one that works) and is capped per TTL window,
with the route answering 429 instead of throwing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(whatsapp-inbox): harden the conversation layer against the review findings

Pre-merge hardening of the unshipped chat layer. Every change below has a
test that fails without it.

Lifecycle and races:
- conversation writes go through updateConversation(), an optimistic
  compare-and-set on updated_at (the trigger makes it a revision counter).
  The ack winner, the answer worker, the pin refresh and the sweep hold
  different claims, so blind whole-jsonb writes resurrected answered
  questions, wiped pending_question and dropped queue entries.
- terminal markStatus writes are guarded on processing_status='processing'
  so a losing worker cannot overwrite the winner's 'done' and null its
  inbox_item_id.
- the message -> inbox item path is idempotent: a pre-check plus a 23505
  fallback adopt the item a concurrent worker created, instead of throwing
  after the WORM document is already committed.
- PROCESSING_STUCK_MS 90s -> 5 min. The enforced step budget of one media
  row already exceeds 90s, so the sweep was re-claiming live workers.
- sweep 2b re-arms only when the conversation itself has been quiet, not
  just the rows: pending_ack=false plus unacked rows is also the state of a
  live finalize, which produced a duplicate combined ack.
- pin expiry re-checks against fresh state instead of writing back a stale
  whole context, which reverted company choices applied mid-pass.
- askNextQueuedQuestion claims the pop before sending, so two answer
  workers cannot ask the same question twice.

Company question:
- the state is rolled back when the M6 send fails, so the next receipt
  re-asks instead of parking receipts behind a question nobody received.
- applyCompanyChoice claims the open question (company_options) rather
  than the state: a double tap confirms once, a transient membership-query
  error is no longer read as "not a member", and a LATE answer still lands.
- at the 48h TTL the parked receipts are kept, not discarded: options and
  staged rows survive so a late digit or tap still files them, and only
  rows past Meta's ~30-day media window get the terminal marker.
- an out-of-range digit or a typed company name now gets the options
  repeated instead of silence or the "I cannot answer questions" reply.

Inline dispositions:
- stop/start/byt/company answers run their side effect BEFORE the terminal
  wamid row, with a SELECT pre-check for dedupe. Writing the row 'done'
  first made them at-most-once: a crash in between lost the action forever.

Copy and answers:
- acks state the extracted currency instead of labelling every total 'kr'.
- M17 stops promising "about 10 minutes" when the daily quota tripped.
- M18 is sent once per message tracked by the outbound row, so a file
  whose first attempt died still reaches the sender, including from the
  max-attempts path.
- M11 no longer claims the number is disconnected: 'stopp' pauses, and
  muted senders now persist no chat content at all.
- 'byt' is recognized in every state but awaiting_company (m6-confirm
  teaches the word, and it was being stored as answer data instead).
- text sent while a re-send question is open is kept as a note on THAT
  receipt with the question left open, instead of binding to another
  receipt's question.
- a quoted reply wins over the pending question and is appended when the
  quoted question is already answered, so corrections stop landing on the
  wrong receipt.
- context answers keep raw_answer + answered_at like representation does.
- finalizeBurst checks the send result: on failure it rolls the question
  back and leaves the rows unacked for the sweep.

PII:
- the sender's plaintext number is stripped from raw_payload before it is
  persisted; replies decrypt the link's phone_enc instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(whatsapp-inbox): record the erasure path and the hardening decisions

RoPA gains the account-deletion row (immediate, not via the cron: the
auth.users cascade never fires because the row is tombstoned) plus the
two new security measures, and its "never in the clear" phone claim is
now true of the stored payload. DECISIONS.md records the non-obvious
calls: revoke-not-delete on erasure, commit-then-roll-back for the
company question, keeping expired company choices answerable, the
compare-and-set conversation write, effect-before-terminal-row for inline
dispositions, honest M11 copy, and the raw_payload redaction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(whatsapp-inbox): stop the answer re-claim from following a confirm with M16

A worker that died after applying an answer and sending its confirmation
leaves the row 'processing'. The sweep re-runs it, resolveAnswerTarget
finds the question already answered, and the user got "I did not
understand" immediately after the confirmation they had just received.
The fallback is now first-attempt only.

The catch comment claiming the sweep retries these rows is corrected
too: 'error' is terminal for the sweep, and nothing on the answer path
throws anyway (interpretChatAnswer degrades, sends never throw,
supabase-js returns errors), so the catch is a programming-error net.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(whatsapp-inbox): drop the amount floor on the representation question

The Swedish compliance review on #1340 caught a real error in the trigger
rules: the representation question only fired above 150 kr, but the duty to
document deltagare and syfte is what makes the expense deductible at all
(BFL 5 kap 6-7 §) and it is not conditioned on any amount. The 300 kr per
person figure I had in mind is the VAT-deduction base cap, a different rule.
A 120 kr business lunch would have been booked with no participant trail,
which is exactly the deduction Skatteverket denies later.

Noise stays bounded by the triggers that were already there: the question
fires only for receipt-shaped documents from restaurant, cafe or hotel
merchants, at most once per receipt, twice per burst and six times per
sender per day, and a single "nej" dismisses it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
2026-08-05 15:47:23 +02:00

763 lines
32 KiB
YAML

# Record of Processing Activities (GDPR Art. 30)
#
# Each entry documents one processing activity. Field shape follows EDPB
# Guidelines 9/2022 §3 (controller-side RoPA): purposes, lawful basis,
# data subject + data category, recipients, transfer mechanism, retention,
# security measures. Add an entry whenever a new flow ships that touches
# personal data: onboarding, integrations, support, or regulator filings.
processing_activities:
- id: invoices.delivery_history
name: Leveranshistorik för kundfakturor
purpose: >-
Dokumentera att en kundfaktura skickades, när leveransförsöket skedde,
vilken mottagardomän som användes och vilken exakt PDF som skickades.
Dashboardens listvy visar endast maskerade mottagardomäner och
driftmetadata. Fullständigt meddelandeinnehåll stannar på serversidan.
lawful_basis: art_6_1_b_and_c
special_category_basis: null
controller: gnubok-tenant
processor: supabase_and_resend
data_subjects:
- customer_contact
- company_member
data_categories:
- user.contact.email
- user.communication_content
- user.activity_timestamp
- user.document
recipients:
- name: Supabase
country: EU
role: processor
- name: Resend
country: US
role: processor
international_transfers:
applicable: true
mechanism: scc_2021_c2p
note: Resend processes the outbound delivery under SCC Module 2.
retention:
duration: through_seventh_calendar_year_after_fiscal_year_end
basis: bfl_7_kap_and_gdpr_storage_limitation
stored_in:
- invoice_deliveries
- document_attachments
security_measures:
- active_company_route_context
- rls_company_scoped
- masked_recipient_domains_in_list_api
- no_message_content_in_browser_list_api
- exact_sent_pdf_worm_protection
- metadata_only_immutable_audit_log
- daily_post_retention_pii_redaction
- signed_provider_delivery_webhook_only
- masked_recipient_addresses_in_provider_reason_text
- id: customer.private_identity
name: Personnummer för privatkund
purpose: >-
Identifiera en privatkund när företagets avtal, fakturering eller
kundadministration kräver en entydig identitet. Personnumret får endast
sparas på kundtypen privatkund. Samtliga listnings- och exportytor i API
och UI exponerar det endast maskerat (fyra sista siffrorna). Undantaget är
en enskild uppslagsyta (GET /api/customers/{id}/personal-number) som
returnerar hela värdet för en kund i taget, kräver skrivroll och loggas
med aktör men aldrig med värdet, så att den som registrerat ett
personnummer kan kontrollera vad som faktiskt sparats.
lawful_basis: art_6_1_b
special_category_basis: null
controller: gnubok-tenant
processor: supabase
data_subjects:
- customer
data_categories:
- user.government_id
recipients:
- name: Supabase
country: EU
role: processor
international_transfers:
applicable: false
mechanism: null
note: EU-only processor.
retention:
duration: customer_relationship_or_7y_if_accounting_record
basis: contract_and_bfl_7_kap
stored_in:
- customers.personal_number
- retained_invoice_documents_when_required
security_measures:
- aes_256_gcm_field_encryption
- masked_api_and_ui_output_last4_only
- write_role_required
- rls_company_scoped
# Replaces the previous `no_full_value_read_endpoint`. A single explicit
# drill-in now exists (GET /api/customers/{id}/personal-number): the
# value was otherwise write-only, so a user could store a personnummer
# and never verify what had been stored. Every listing surface still
# masks; the drill-in requires the write role, returns one customer at a
# time, and logs actor + customer id without the value.
- full_value_read_only_via_single_customer_drill_in
- full_value_read_requires_write_role
- full_value_read_logged_with_actor_never_value
- id: agi.submit
name: AGI inlämning till Skatteverket
purpose: >-
Lämna in lagstadgad arbetsgivardeklaration på individnivå (AGI) till
Skatteverket varje månad (Skatteförfarandelagen 26 kap.). Innefattar
huvuduppgift (HU) och individuppgifter (IU) per anställd. Skatteverkets
kvittens sparas i response_data inklusive signeradAv: personnummer för
den som BankID-signerade inlämningen. Ändamålet är bevis för vem som
juridiskt undertecknade deklarationen samt behandlingshistorik; BFL
5 kap. 6 § och BFNAR 2013:2 kap. 8 kräver att räkenskapsinformationen
bevarar den faktiska ingivaren, medan själva inlämningen styrs av SFL.
lawful_basis: art_6_1_c # legal obligation
special_category_basis: null
controller: gnubok-tenant
processor: anthropic-na # software supplier; Skatteverket is recipient, not processor
data_subjects:
- employee
- company_signatory # firmatecknare/deklarationsombud (BankID-signerare av kvittensen)
data_categories:
- user.government_id # personnummer (anställda i IU; signerarens i response_data.signeradAv)
- user.name
- user.financial.employment # gross salary, tax withheld, benefits
- user.financial.tax # avgifter, sjuklönekostnad
recipients:
- name: Skatteverket
country: SE
role: legal_recipient
international_transfers:
applicable: false
mechanism: null
note: >-
Sweden-to-Sweden processor-to-public-authority flow. Adequacy decision
not applicable; no third-country transfer.
retention:
duration: 7y
basis: bfl_7_kap # BFL 7 kap. 2 §: räkenskapsinformation
stored_in:
- agi_declarations.xml_content
- agi_declarations.individuppgifter
- agi_declarations.response_data # SKV kvittens incl. signeradAv (signerarens pnr); access via company-membership RLS
- skatteverket_api_audit_log
security_measures:
- encryption_at_rest_supabase
- rls_company_scoped
- tls_1_3_to_skatteverket
- bankid_signing_required_for_filing
- immutable_audit_log
- personnummer_at_rest_encryption
- id: agi.kontrollera.preflight
name: AGI pre-flight kontrollera (HU/IU)
purpose: >-
Skicka enskild HU eller IU som JSON till Skatteverkets kontrollera-
ändpunkt (v1.7 §7/§8) för dry-run-validering av felmeddelanden och
STOPP-/AVVISANDE-status innan ett fullständigt underlag laddas upp.
Skatteverket lagrar inget från denna kontroll; svaret driver vår UI
som låter användaren rätta felaktigheter i lönekörningen lokalt.
lawful_basis: art_6_1_c # legal obligation (ancillary to AGI filing)
special_category_basis: null
controller: gnubok-tenant
processor: anthropic-na
data_subjects:
- employee
data_categories:
- user.government_id # personnummer (BetalningsmottagarId)
- user.financial.employment # gross salary, tax withheld, benefits
recipients:
- name: Skatteverket
country: SE
role: legal_recipient
international_transfers:
applicable: false
mechanism: null
note: Sweden-to-Sweden flow; no third-country transfer.
retention:
# Skatteverket retains nothing for kontrollera calls. Our outbound
# audit row in skatteverket_api_audit_log records who-called-what-when
# but NOT the payload body, minimising at-rest footprint.
duration: 7y
basis: bfl_7_kap
stored_in:
- skatteverket_api_audit_log
security_measures:
- strict_zod_allow_list_input_validation
- request_size_cap_64kb
- rbac_company_member_role_check
- rls_company_scoped
- tls_1_3_to_skatteverket
- immutable_audit_log
- no_payload_body_persisted
- id: moms.declaration
name: Momsdeklaration till Skatteverket
purpose: >-
Lämna in lagstadgad momsdeklaration (utkast, lås och signering) till
Skatteverkets momsdeklaration-API (Mervärdesskattelagen 17 kap. 24 §).
lawful_basis: art_6_1_c
special_category_basis: null
controller: gnubok-tenant
processor: anthropic-na
data_subjects:
- business_owner
data_categories:
- user.government_id # orgnummer (juridiska personer) eller pnr
- user.financial.tax # rutor 05-62
recipients:
- name: Skatteverket
country: SE
role: legal_recipient
international_transfers:
applicable: false
mechanism: null
note: Sweden-to-Sweden flow; no third-country transfer.
retention:
duration: 7y
basis: bfl_7_kap
stored_in:
- extension_data (utkast metadata)
- skatteverket_api_audit_log
security_measures:
- rls_company_scoped
- tls_1_3_to_skatteverket
- bankid_signing_required_for_filing
- immutable_audit_log
- id: psd2.cash_account_mirror
name: PSD2-konton speglas till cash_accounts
purpose: >-
När en bank-anslutning via Enable Banking returnerar kontolista efter
lyckad PSD2-consent kopieras kontometadata (IBAN, valuta, kontonamn,
external_uid) till cash_accounts så att avstämning, motkontoresolver
och __PRIMARY_SEK__-sentinel kan rutta verifikat utan att läsa JSONB
från bank_connections.accounts_data vid varje fråga.
lawful_basis: art_6_1_b # contract (PSD2 consent + bookkeeping service)
special_category_basis: null
controller: gnubok-tenant
processor: anthropic-na
data_subjects:
- business_owner
data_categories:
- user.financial.bank_account # IBAN
- user.contact # account name (kontotitel)
recipients:
- name: Supabase
country: EU
role: processor
international_transfers:
applicable: false
mechanism: null
note: EU-only processor; no third-country transfer.
retention:
duration: consent_lifetime
basis: psd2_consent
stored_in:
- cash_accounts
- bank_connections.accounts_data
security_measures:
- rls_company_scoped
- data_minimization_no_balance_on_callback
- failure_emitted_to_event_log
- id: transactions.counterparty_iban
name: Motpartens IBAN på transaktioner
purpose: >-
Spara motpartens IBAN på transaktionsraden så att own-account-detector
kan identifiera överföringar mellan företagets egna konton (1930 → 1932
etc.) och bokföra båda benen automatiskt istället för att felbokföra
utflödet som extern kostnad. Krävs även för payment-matchning mot
leverantörsfakturor.
lawful_basis: art_6_1_b # contract (bookkeeping service)
special_category_basis: null
controller: gnubok-tenant
processor: anthropic-na
data_subjects:
- business_owner
- counterparty
data_categories:
- user.financial.bank_account # counterparty IBAN
recipients:
- name: Supabase
country: EU
role: processor
international_transfers:
applicable: false
mechanism: null
note: EU-only processor.
retention:
duration: 7y
basis: bfl_7_kap
stored_in:
- transactions.counterparty_iban
security_measures:
- rls_company_scoped
- immutable_after_post
- id: skattekonto.drift_alert
name: Skattekonto-drift via e-post
purpose: >-
Underrätta företagets kontaktadress när det cachade Skatteverket-saldot
avviker från GL 1630 utöver konfigurerad tolerans (> 1 SEK), så
bokföraren kan granska skattekonto-raderna. E-postmeddelandet
innehåller ingen finansiell siffra utan en länk till autentiserad
dashboard; mottagaren valideras mot company_members innan utskick.
lawful_basis: art_6_1_f # legitimate interest (bookkeeping accuracy)
special_category_basis: null
controller: gnubok-tenant
processor: resend
data_subjects:
- business_owner
- company_member
data_categories:
- user.contact.email
recipients:
- name: Resend
country: US
role: processor
international_transfers:
applicable: true
mechanism: scc_2021_c2p
note: >-
Resend (US): SCC Module 2 (controller-to-processor). Outbound
payload limited to ett notifieringsmail utan finansiella belopp;
TIA dokumenterad i .compliance/tia/resend.md.
retention:
duration: 30d
basis: event_log_ttl
stored_in:
- event_log
security_measures:
- recipient_membership_check_before_send
- no_financial_figures_in_body
- tls_to_resend
- rls_company_scoped
- id: ai.inference
name: AI-inferens (kategorisering + dokumenttolkning) via Amazon Bedrock
purpose: >-
Föreslå bokföringskategori för banktransaktioner och tolka/extrahera
uppladdade underlag (kvitton, leverantörsfakturor) med Anthropic
Claude-modeller körda i Amazon Bedrock. Endast aktiv när tenanten
uttryckligen aktiverat AI-funktioner: kärntjänsten (bokföring, fakturor,
moms, rapporter) fungerar fullt ut utan AI.
lawful_basis: art_6_1_a # consent: opt-in, AI features off by default
special_category_basis: null
controller: gnubok-tenant
processor: aws-bedrock
data_subjects:
- business_owner
- counterparty # namn/belopp på uppladdade underlag
data_categories:
- user.financial # transaktionsdata skickad för kategorisering
- user.document # uppladdade kvitton/fakturor för OCR/extraktion
recipients:
- name: Amazon Web Services (Amazon Bedrock)
country: EU
role: processor
international_transfers:
applicable: false
mechanism: null
note: >-
Inferens körs i AWS-regionen eu-north-1 (Stockholm): ingen överföring
till tredje land. AWS DPA + SCC + DPF-certifiering finns som
skyddsmekanism. Prompter lagras ej hos Bedrock efter anropet och används
ej till modellträning.
retention:
duration: none_at_processor
basis: no_retention_bedrock # prompt not persisted by Bedrock post-inference
stored_in:
- ai_usage_tracking # usage metadata only (tokens/cost), no payload body
security_measures:
- opt_in_consent_required
- eu_region_inference_eu_north_1
- prompts_not_retained_after_inference
- not_used_for_model_training
- rls_company_scoped
- tls_to_bedrock
- id: mcp.telemetry
name: MCP/agent-telemetri i event_log
purpose: >-
Varje MCP-verktygsanrop loggar metadata (verktygsnamn, felkod,
felmeddelande max 500 tecken, latens, aktör, session, frivillig
distributionskanal-markör, t.ex. 'openclaw' via X-Gnubok-Client-header
eller ?client=-parameter, sanerad mot allow-list och aldrig använd för
auktorisation) och varje skill-laddning loggar slug/tier till event_log.
Syftet är tillförlitlighetsanalys av agentgränssnittet: felfrekvens per
verktyg, adoption per distributionskanal, korrelation mellan laddade
skills och efterföljande fel, samt agenters egenrapporterade feedback
(agent.feedback). Inga verktygsargument eller verktygsresultat
persisteras.
lawful_basis: art_6_1_f # legitimate interest (service reliability/improvement)
special_category_basis: null
controller: gnubok-tenant
processor: supabase
data_subjects:
- business_owner
data_categories:
- user.unique_id # userId, actorId (API-nyckel-id), sessionId
- user.contact # actorLabel (användarvald API-nyckeletikett)
recipients:
- name: Supabase
country: EU
role: processor
international_transfers:
applicable: false
mechanism: null
note: EU-only processor; no third-country transfer.
retention:
# Differentiated TTL via /api/events/cleanup/cron: mcp.*/agent.*-rader
# behålls 180 dagar (felfrekvens-trender kräver mer än leveransfönstret);
# övriga event_log-rader (leveranshändelser) 30 dagar.
duration: 180d
basis: legitimate_interest_reliability
stored_in:
- event_log
security_measures:
- rls_user_scoped_select # event_log SELECT: auth.uid() = user_id
- service_role_only_writes
- error_message_truncated_500_chars
- no_tool_args_or_results_persisted
- id: mcp.company_customer_tools
name: MCP-verktyg för företagsinställningar och kunder
purpose: >-
Låta en uttryckligen behörig användare läsa nödvändiga företags- och
kunduppgifter samt förbereda ändringar för användarens godkännande.
Verktygssökningen gnubok_search_tools returnerar endast verktygsmetadata
och JSON-schema, aldrig företags- eller kundposter. Läsresultat begränsas
till det aktiva företaget och till verktygets dokumenterade fält.
lawful_basis: art_6_1_b
special_category_basis: null
controller: gnubok-tenant
processor: supabase
data_subjects:
- business_owner
- company_member
- customer
data_categories:
- user.name
- user.contact
- user.financial.bank_account
recipients:
- name: Supabase
country: EU
role: processor
international_transfers:
applicable: false
mechanism: null
note: EU-only processor.
retention:
duration: pending_operation_lifecycle
basis: contract_and_user_approval
stored_in:
- pending_operations
security_measures:
- api_key_scope_companies_read_or_write
- api_key_scope_customers_read_or_write
- active_company_filter_on_every_query
- rls_company_scoped
- search_returns_tool_metadata_only
- writes_staged_for_explicit_user_approval
- plaintext_personnummer_blocked_from_staging
- id: mcp.async_task_handles
name: MCP-uppgiftshandtag för långkörande verktygsanrop
purpose: >-
Låta en behörig MCP-klient hämta resultatet av ett långkörande
verktygsanrop (t.ex. generering av arkivpaket) via ett hållbart
uppgifts-id i stället för att hålla anslutningen öppen. Raden lagrar
verktygets svar tills klienten hämtat det; endast den skapande
användaren kan läsa, och utgångna rader rensas löpande vid nya anrop.
lawful_basis: art_6_1_b
special_category_basis: null
controller: gnubok-tenant
processor: supabase
data_subjects:
- business_owner
- company_member
data_categories:
- user.financial
- user.activity_timestamp
recipients:
- name: Supabase
country: EU
role: processor
international_transfers:
applicable: false
mechanism: null
note: EU-only processor.
retention:
duration: 1h
basis: transient_tool_call_state_storage_limitation
stored_in:
- mcp_tasks
security_measures:
- rls_user_scoped_select # mcp_tasks SELECT: auth.uid() = user_id
- service_role_only_writes
- creator_scoped_task_polling # tasks/get requires the creating user
- expired_rows_purged_on_creation
- id: arsredovisning.signature_evidence
name: Underskriftsbevis för årsredovisning
purpose: >-
Bevara vem som undertecknade en låst årsredovisningsversion, i vilken
roll, med vilken metod och på vilket datum. Bevisreferensen är endast en
ogenomskinlig identifierare med prefix archive:, document: eller receipt:.
Den får inte innehålla namn, e-post, personnummer eller fritext.
lawful_basis: art_6_1_c
special_category_basis: null
controller: gnubok-tenant
processor: supabase
data_subjects:
- company_signatory
data_categories:
- user.name
- user.professional_role
- user.activity_timestamp
recipients:
- name: Supabase
country: EU
role: processor
international_transfers:
applicable: false
mechanism: null
note: EU-only processor.
retention:
duration: 7y
basis: bfl_7_kap_and_arl_signing_evidence
stored_in:
- arsredovisning_signature_requests
- annual_report_versions
security_measures:
- rls_company_scoped
- service_role_only_signature_transitions
- version_company_period_and_state_validation
- signed_rows_immutable
- structured_opaque_evidence_reference
- future_and_pre_finalization_dates_rejected
- id: arsredovisning.bolagsverket.submit
name: Digital inlämning av årsredovisning till Bolagsverket
purpose: >-
Lämna in årsredovisningen (iXBRL) digitalt till Bolagsverkets eget
utrymme (ÅRL 8 kap.). Avsändarens och undertecknarens personnummer
krävs av Bolagsverkets API (skapa-inlamningtoken, lamna-in) och
används endast transient i anropen. Varken personnummer eller hash av
personnummer sparas i inlämningsposten. Fastställelseintyget signeras
med e-legitimation hos Bolagsverket, aldrig i appen.
lawful_basis: art_6_1_c # legal obligation (ÅRL filing duty)
special_category_basis: null
controller: gnubok-tenant
processor: anthropic-na # software supplier; Bolagsverket is recipient, not processor
data_subjects:
- business_owner # avsändare/undertecknare (styrelseledamot, VD)
data_categories:
- user.government_id # personnummer, endast transient till Bolagsverket
- user.name # undertecknarens namn
- user.contact # undertecknarens/kvittens e-post
- user.financial # årsredovisningens finansiella innehåll
recipients:
- name: Bolagsverket
country: SE
role: legal_recipient
international_transfers:
applicable: false
mechanism: null
note: Sweden-to-Sweden flow to a public authority; no third-country transfer.
retention:
duration: 7y
basis: bfl_7_kap # filed .xhtml archived as räkenskapsinformation
stored_in:
- arsredovisning_submissions # status, idnummer and checksums, no PNR values or hashes
- document_attachments # the exact filed iXBRL bytes (WORM)
security_measures:
- mtls_client_certificate_to_bolagsverket
- mtls_key_env_only_injected_from_secret_manager # never in settings/DB; see .env.example custody note
- pinned_endpoint_allowlist_per_environment # HOSTS map in client.ts; not env-configurable
- personnummer_and_hash_never_persisted
- personnummer_never_logged
- webhook_secret_constant_time_validation
- rls_company_scoped
- immutable_status_machine_trigger
- id: product.analytics
name: Produktanalys, sessionsinspelning, felrapportering, enkäter och supportärenden (PostHog)
purpose: >-
Förstå hur tjänsten faktiskt används (var användare fastnar i
onboarding, vilka flöden som avbryts), fånga klientfel och serverfel så
de kan åtgärdas, och ställa riktade produktfrågor. Endast hostad drift:
self-hosted-installationer laddar aldrig PostHog (isAnalyticsEnabled()
kortsluter på NEXT_PUBLIC_SELF_HOSTED, och ingen token bakas in i
Docker-imagen). Sandbox-/demoföretag identifieras aldrig.
lawful_basis: art_6_1_f # legitimate interest (produktförbättring och felsökning)
special_category_basis: null
controller: gnubok-tenant
processor: posthog-eu
data_subjects:
- business_owner
- company_member
data_categories:
- user.contact.email # person property via identify(), aldrig i event-properties
- user.name # profiles.full_name
- user.behavior # sidvisningar, klick, händelser, sessionsinspelning
- user.device # user agent, skärmstorlek, IP (trunkeras av PostHog)
# Supportärenden: fritext som användaren SJÄLV skriver till supporten via
# in-app-dialogen (components/ui/support-link.tsx). Till skillnad från
# analysdata bär ärendet meddelandetexten, eftersom det är hela poängen
# med att skriva till supporten. Kan i praktiken innehålla bokförings-
# detaljer som användaren väljer att beskriva. Widgeten är avstängd:
# endast vår egen dialog kan öppna ärenden.
- user.content.support # fritext i supportmeddelanden, användarinitierat
# EJ organisationsnummer: för enskild firma ÄR orgnr innehavarens
# personnummer. buildGroupProperties() vägrar skicka det och ett
# enhetstest låser fast beteendet.
recipients:
- name: PostHog
country: DE
role: processor
international_transfers:
applicable: false
mechanism: null
note: >-
PostHog Cloud EU (Frankfurt). EU-baserat biträde; ingen
tredjelandsöverföring. DPA tecknat; SCCs gäller för eventuella
underbiträden utanför EES.
retention:
duration: posthog_project_retention
basis: processor_configured # sätts i PostHog-projektet, ej i denna kodbas
stored_in:
- posthog_eu # externt hos biträdet; inget lagras i vår databas
security_measures:
- session_replay_masks_all_text # maskTextSelector '*' utöver maskAllInputs
- session_replay_masks_all_inputs
- org_number_never_transmitted # låst av test i lib/analytics/__tests__
- no_pii_in_event_properties # PII endast som person properties via identify()
# persistence: 'memory' -> ingen analysdata och inga kakor på enheten.
# TVÅ undantag, båda funktionell UI-state utan personuppgifter, båda
# skrivna direkt till localStorage av PostHogs egna moduler förbi
# persistence-inställningen: seenSurvey_<id> ("visa inte igen") och
# ph_conv_<token> = {"widgetSessionId": uuid} (hitta tillbaka till sitt
# eget ärende i samma webbläsare). ph_conv skapas vid sidladdning, alltså
# innan användaren faktiskt kontaktar supporten. Verifierat i prod.
- no_analytics_data_or_cookies_on_device
- only_functional_ui_state_persisted # seenSurvey_*, ph_conv_* (uuid, ingen PII)
# Radering (A.8.10), se .compliance/Data_Classification_Handling.md:
# ph_conv_* raderas vid utloggning (resetAnalyticsIdentity ->
# posthog.reset() -> conversations-managerns egen reset). seenSurvey_*
# har ingen raderingstrigger: ingen PostHog-bundle itererar över
# localStorage, så SDK:n kan inte hitta nycklarna. Accepterat: värdet är
# "true" under ett ogenomskinligt survey-id, utan personuppgifter.
- ph_conv_cleared_on_logout # hindrar delad enhet från att ärva ärendesession
- same_origin_reverse_proxy # /rl-rewrite; ingen tredjepartsvärd i CSP
- sandbox_companies_never_identified
- disabled_entirely_when_self_hosted
- server_side_payloads_redacted_before_send # lib/observability/redact.ts
- support_widget_disabled # endast egen UI via conversations-API
- support_ticket_identity_hmac_signed # POSTHOG_SECRET_API_KEY, serverside
- support_message_body_never_in_event_properties # breadcrumb bär endast subject
- email_remains_the_delivery_guarantee # ärendet är ett komplement, ej leveransväg
- id: whatsapp.receipt_intake
name: Kvittoinlämning och klargörande dialog via WhatsApp
purpose: >-
Ta emot kvitton som foto eller PDF skickade till Accounteds delade
WhatsApp-nummer och landa dem i dokumentinkorgen (Underlag) som
räkenskapsinformation, samt föra en kort klargörande dialog i samma
chatt: bekräfta mottagning, fråga vilket företag underlaget tillhör och
samla in de representationsuppgifter Skatteverket kräver (deltagarnas
namn och företag samt syfte). Avsändare är enbart befintliga
Accounted-användare som själva knutit sitt nummer med en engångskod;
okända nummer får ett enda begränsat standardsvar och behandlas aldrig
vidare (ingen AI, ingen mediahämtning, inget innehåll sparas).
Deltagarnamn i representationssvar är tredjepartsuppgifter som samlas
in från användaren, inte från de registrerade själva; individuell
information till varje deltagare vore oproportionerlig i förhållande
till den lagstadgade dokumentationsplikten och undantaget i art.
14.5 b tillämpas (uppgifterna framgår av verifikationen, som bevaras
enligt BFL).
# art 6.1 b: tjänsten (mottagning, dialog, inkorgsleverans).
# art 6.1 c: BFL 7 kap: kvittot bevaras 7 år som räkenskapsinformation.
# art 6.1 f: tredjeparts deltagarnamn i representationssvar (med
# art 14.5 b-undantaget dokumenterat i purpose ovan).
lawful_basis: art_6_1_b_c_and_f
special_category_basis: null
controller: gnubok-tenant
processor: supabase_and_meta
data_subjects:
- business_owner
- company_member
- representation_attendee # tredjepart: namn + företag i representationssvar
data_categories:
- user.contact.phone_number # HMAC-hash med peppar + AES-256-GCM + mask; aldrig i klartext i DB
- user.communication_content # chattext och webhook-payload utan avsändarnummer, 90 dagars retention
- user.document # kvittofiler, 7 år WORM enligt BFL 7 kap
- user.name # deltagarnamn i representationssvar (tredjepart)
- user.activity_timestamp
recipients:
- name: Meta Platforms Ireland Ltd (WhatsApp Business Platform, Cloud API)
country: IE
role: processor
- name: Supabase
country: EU
role: processor
# AI-tolkningen av kvittobilder och fritextsvar sker via Amazon Bedrock
# och täcks av den befintliga aktiviteten ai.inference ovan.
international_transfers:
applicable: true
mechanism: scc_2021_c2p
note: >-
Meta Platforms Ireland behandlar meddelandena under WhatsApp Business
dataskyddsvillkor med EU SCC-addendum (Module 2) för vidareöverföring
till Meta Platforms Inc. (US). Cloud API Local Storage är aktiverat
med region DE, så meddelandeinnehåll lagras i vila inom EU hos Meta.
retention:
# Differentierad per datakategori; verkställs dagligen av
# /api/extensions/whatsapp-inbox/retention/cron:
# chattranskript (body_text, raw_payload) 90 dagar, sedan rensas
# innehållet men radskelettet (wamid, riktning, tidsstämplar,
# status, inbox-koppling) bevaras som behandlingsspår
# okända avsändares rader 30 dagar, sedan radering
# engångskoder 24 h efter utgång, radering
# telefonkoppling (phone_enc) tills användaren återkallar,
# därefter kryptoshredning 90 dagar efter revokering (phone_hash och
# phone_masked bevaras för unikhetshistorik respektive visning)
# kvittofilen 7 år, WORM (BFL 7 kap),
# hanteras av dokumentarkivet, ej av denna cron
# kontoradering (art 17) omedelbart, inte via cron:
# anonymize_user_account revokerar och kryptoshreddar kopplingen,
# nollställer body_text/raw_payload på kopplingens meddelanden och
# raderar engångskoder (migration 20260803090000). auth.users
# gravsätts i stället för att raderas, så tabellens ON DELETE
# CASCADE utlöses aldrig av sig själv
duration: differentiated_per_data_category
basis: gdpr_storage_limitation_and_bfl_7_kap
stored_in:
- whatsapp_messages
- whatsapp_phone_links
- whatsapp_link_codes
- whatsapp_sender_rate_counters
- whatsapp_conversations
- invoice_inbox_items.channel_context
- document_attachments # kvittofilen, WORM
security_measures:
- phone_number_hmac_peppered_hash_lookup_only
- phone_number_aes_256_gcm_at_rest
- phone_number_masked_in_ui
- dedicated_whatsapp_key_material_not_bankid_keys
- webhook_signature_verified_over_raw_body
- service_role_only_tables_rls_no_policies
- unknown_senders_no_content_persistence_no_llm
- pre_binding_sender_rate_limits
- daily_retention_cron_with_crypto_shred
- llm_reads_answers_as_untrusted_data_no_tools
- receipt_files_worm_protected_bfl
- opt_out_stop_keyword_honored
- muted_senders_no_content_persistence
- account_deletion_revokes_and_shreds_whatsapp_link