Files
accounted/.compliance/ropa.yaml
T
Jakob Wennberg cce0de5704 feat(mcp): already-explained voucher guard at stage and commit for match_batch_allocate (#2294) (#2346)
* feat(mcp): already-explained voucher guard at stage and commit for match_batch_allocate

The dashboard match-batch route refused BATCH_TX_POSSIBLE_DUPLICATE when
posted, unlinked vouchers already summed to the bank row (PR #2300), but the
MCP door (gnubok_match_batch_allocate staging + commitMatchBatchAllocate)
called the RPC with no guard, so an agent could book a Bankgirot aggregate a
second time. The detector existed once; the guard lived in one door.

One shared decision helper, lib/invoices/already-explained-guard.ts, now
sits on top of the existing detectors (no fork) and is called by the
dashboard route, the MCP staging tools and the commit executors:

- gnubok_match_batch_allocate refuses to stage, coded
  BATCH_TX_POSSIBLE_DUPLICATE, naming the vouchers, the reconcile_match /
  link_transaction_to_journal_entry call that resolves the row, and the
  exact force + expected_journal_entry_ids binding.
- commitMatchBatchAllocate runs the same guard before the RPC and
  re-validates a staged force binding against the set detected at commit,
  so a stale approval cannot book a duplicate; 409 auto-rejects with the
  vouchers in result_data.
- force + expected_journal_entry_ids on the tool mirror MatchBatchSchema;
  an honoured override stages with a compliance_warning and, after the
  booking succeeds, writes BankTransactionDuplicateDismissed to
  behandlingshistorik (dashboard route included; it only logged before).
- gnubok_match_transaction_to_invoice and commitMatchTransactionInvoice get
  the dashboard's 1:1 soft-duplicate guard (MATCH_INVOICE_POSSIBLE_DUPLICATE
  / MATCH_INVOICE_FORCE_CANDIDATE_MISMATCH) with force +
  expected_journal_entry_id; at commit it runs before the storno.
- Registry: both duplicate codes gain retryable: false and a remediation.

Catalog payload held under the 60K ceiling by trimming the two tools' own
descriptions (59 988 measured, ledger entry in payload-size.bench.test.ts).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SaJfqNi4VmsG8FMKq99G6

* docs(decisions): record the 2026-09-06 ten-issue batch's first-principles choices

Carries the DECISIONS.md lines for PRs #2337 #2339 #2340 #2341 #2342 #2343 #2344 #2345 #2346 #2347 in one place so the ten branches do not conflict on this file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SaJfqNi4VmsG8FMKq99G6

* fix(mcp): refuse an unverifiable forced override, surface a failed duplicate check, validate the binding (#2294 review)

Review round on PR #2346 (CodeRabbit + compliance):

- guardAlreadyExplained returned 'clear' when the detector threw even with
  force=true, so a forced 1:N override could book without re-validating
  expected_journal_entry_ids and left no behandlingshistorik record. It now
  returns a distinct 'unverifiable' outcome under force (mirrors
  guardDuplicatePaymentVoucher); the dashboard route, the MCP staging tool
  and the commit executor all refuse it with the new registry code
  BATCH_TX_EXPLAINED_CHECK_FAILED (409, retryable, remediation). Regression
  tests on every caller.
- A detector failure without force still fails open at stage time, but no
  longer silently: the tools track onDetectError and stage a
  complianceNote, so preview_data.compliance_warning is set on both
  match_batch_allocate (GenericPreview renders it) and
  match_transaction_invoice (MatchTransactionInvoicePreview now renders
  data.compliance_warning through AttnLine).
- expected_journal_entry_ids / expected_journal_entry_id are validated at
  the MCP boundary (array of 1 to 10 non-empty strings / non-empty string)
  and refused with VALIDATION_ERROR instead of being silently filtered.
  No schema description text added: catalog payload unchanged.
- RoPA: .compliance/ropa.yaml gains bookkeeping.duplicate_dismissal_history
  for the BankTransactionDuplicateDismissed record (Art. 6(1)(c), BFNAR
  2013:2 p. 9.16, retention per BFL 7 kap, stored in processing_history).

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

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 18:56:36 +02:00

900 lines
38 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: moms.declaration_status_read
name: Avläsning av inlämnad momsdeklaration och beslut från Skatteverket
purpose: >-
Läsa tillbaka en redan inlämnad momsdeklaration (inlamnat) och
Skatteverkets beslut (beslutat) för en period, på begäran av en
medlem i företaget via v1 REST-API:t eller MCP, så att bokföringen
kan stämmas av mot faktiskt inlämnade uppgifter. Läsning sker live
mot Skatteverket; deklarationsinnehållet lagras inte hos oss, endast
en audit-rad om att anropet gjordes.
lawful_basis: art_6_1_b # service provision: read-back requested by the customer
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 i det inlämnade underlaget/beslutet
recipients:
- name: Skatteverket
country: SE
role: legal_recipient
international_transfers:
applicable: false
mechanism: null
note: Sweden-to-Sweden flow; no third-country transfer.
retention:
# Response bodies are returned to the caller and never persisted.
# Only the outbound call metadata lands in the audit log.
duration: 7y
basis: bfl_7_kap
stored_in:
- skatteverket_api_audit_log
security_measures:
- rls_company_scoped
- rbac_company_member_role_check
- tls_1_3_to_skatteverket
- immutable_audit_log
- no_payload_body_persisted
- upstream_error_bodies_logged_server_side_only
- 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: 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
# Sessionsinspelningen är maskerad som standard: all tangentbords-
# inmatning och all text maskeras, utom appens eget statiska gränssnitt
# (rubriker, navigering, knappar, etiketter), se security_measures.
- 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:
# Maskerat-som-standard (2026-08-17, ersätter det mönsterbaserade läget
# från 2026-08-06): ALL tangentbordsinmatning maskeras (rrweb
# maskAllInputs utan undantagsfunktion) och ALL text maskeras utom
# element taggade data-ph-unmask (statiskt i18n-gränssnitt: rubriker,
# navigering, etiketter, knappar) samt tabellkolumnrubriker. Även
# otaggat gränssnitt maskeras, så felmoden för ny UI är övermaskering,
# aldrig läckage. Taggat gränssnitt mönsterskrubbas dessutom på belopp
# och person-/organisationsnummer. data-ph-mask tvingar maskering och
# vinner över data-ph-unmask; låst av test i lib/analytics/__tests__.
- session_replay_masked_by_default # lib/analytics/replay-masking.ts
- session_replay_masks_all_input_values
- session_replay_masks_amounts_and_identity_numbers_in_chrome
- session_replay_masks_password_inputs
- org_number_never_transmitted_as_group_property # 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
- id: notifications.bookkeeping_digest
name: Daglig e-postsammanfattning "nytt att bokföra"
purpose: >-
Skicka ett dagligt opt-in-mejl till företagsmedlemmar när nya
banktransaktioner eller inkorgsunderlag har kommit in och väntar på
bokföring. Mejlet innehåller endast antal per kategori och en länk till
appen: aldrig belopp, motparter eller transaktionsinnehåll.
lawful_basis: art_6_1_a
special_category_basis: null
controller: gnubok-tenant
processor: supabase_and_resend
data_subjects:
- company_member
data_categories:
- user.contact.email
- user.financial_activity_counts
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: notification_log_row_retained_with_table
basis: gdpr_storage_limitation
stored_in:
- notification_log
- notification_settings
security_measures:
- opt_in_default_false_user_toggle_only
- counts_only_no_amounts_or_counterparties_in_body
- recipient_restricted_to_active_company_members
- claim_based_once_per_day_dedup
- company_name_header_injection_sanitized
- cron_secret_authenticated_trigger
- id: bookkeeping.duplicate_dismissal_history
name: Behandlingshistorik för åsidosatt dubblettkontroll
purpose: >-
Bevara en oföränderlig post i behandlingshistoriken (processing_history,
händelsetyp BankTransactionDuplicateDismissed) när en användare eller en
API-nyckel bokför en banktransaktion trots att dubblettkontrollen visade
att bokförda verifikat redan förklarar den (force=true i dashboarden,
i MCP-verktyget eller vid godkännande av en väntande operation). Posten
gör beslutet rekonstruerbart för revisor och Skatteverket: vilka
verifikat som åsidosattes, belopp, konto, datum och vem som fattade
beslutet. Innehåller endast interna id:n, verifikationsnummer, belopp
och datum; aldrig motpartsnamn, transaktionsbeskrivningar eller
personnummer.
lawful_basis: art_6_1_c # BFL 5 kap. 11 § + BFNAR 2013:2 p. 9.16 (behandlingshistorik)
special_category_basis: null
controller: gnubok-tenant
processor: supabase
data_subjects:
- company_member # the user (or API-key owner) who honoured the override
data_categories:
- user.activity_timestamp
- user.financial.transaction # amount + settlement account of the bank row, no counterparty
recipients:
- name: Supabase
country: EU
role: processor
international_transfers:
applicable: false
mechanism: null
retention:
duration: through_seventh_calendar_year_after_fiscal_year_end
basis: bfl_7_kap_and_gdpr_storage_limitation
stored_in:
- processing_history
security_measures:
- rls_company_scoped
- service_role_insert_only_no_update_or_delete_triggers
- personnummer_pattern_rejected_at_append
- ids_labels_amounts_dates_only_never_descriptions_or_counterparties
- actor_recorded_as_pseudonymous_user_or_api_key_id
- written_only_after_the_booking_succeeded
- id: documents.own_company_supplier_guard
name: Egenbolagskontroll vid dokumentextraktion
purpose: >-
Jämföra AI-extraherade leverantörsfält mot det egna bolagets namn och
organisationsnummer (för enskild firma ett personnummer) så att
användarens eget bolag aldrig föreslås som leverantör när modellen läst
dokumentets kundblock. Jämförelsen sker i applikationskoden efter
extraktionen; identiteten skickas aldrig till AI-modellen och skrivs
inte till någon ny lagringsyta.
lawful_basis: art_6_1_f
special_category_basis: null
controller: gnubok-tenant
processor: supabase
data_subjects:
- business_owner
data_categories:
- user.name # companies.name
- user.government_id # companies.org_number, personnummer för enskild firma
recipients:
- name: Supabase
country: EU
role: processor
international_transfers:
applicable: false
mechanism: null
note: >-
Transient jämförelse i applikationsprocessen; datat lämnar aldrig
befintlig lagring (companies-tabellen) och skickas inte till Bedrock.
retention:
duration: none
basis: transient_in_memory_comparison
stored_in: []
security_measures:
- rls_company_scoped
- not_sent_to_model
- transient_comparison_no_new_storage
- fail_open_to_nulls_on_lookup_error