Data Model
How Amply Structures Information
This document describes Amply's core data model, including organizations, funds, projects, and transactions.
Core Entities
Organizations
Organizations are the primary entities receiving donations:
Organization
├── id: string (org_xxx)
├── name: string
├── legal_name: string
├── type: enum (nonprofit, charity, foundation, fiscally_sponsored)
├── status: enum (pending, verified, suspended)
├── country: string
├── tax_id: string (encrypted)
├── stripe_account_id: string
├── verification_level: enum (standard, enhanced, comprehensive)
├── created_at: timestamp
├── updated_at: timestamp
│
├── profile
│ ├── description: text
│ ├── mission: text
│ ├── logo_url: string
│ ├── website: string
│ └── sdgs: array[1-17]
│
├── settings
│ ├── payout_schedule: enum
│ ├── default_visibility: enum
│ ├── accepts_fundraisers: boolean
│ └── ...
│
└── relationships
├── team_members: [User]
├── funds: [Fund]
└── campaigns: [Campaign]
Funds
Funds are designated pools of money within organizations:
Fund
├── id: string (fund_xxx)
├── organization_id: string
├── name: string
├── type: enum (general, project, restricted, emergency)
├── status: enum (active, closed)
├── balance: integer (cents)
├── currency: string
├── created_at: timestamp
│
├── restrictions
│ ├── purpose: text
│ ├── geographic: string[]
│ └── time_bound: date_range
│
└── relationships
├── projects: [Project]
└── transactions: [Transaction]
Projects
Projects are specific initiatives funded by donations:
Project
├── id: string (proj_xxx)
├── organization_id: string
├── fund_id: string
├── name: string
├── description: text
├── status: enum (planning, active, completed, cancelled)
├── budget: integer
├── spent: integer
├── sdgs: array[1-17]
├── start_date: date
├── end_date: date
│
├── impact_metrics
│ ├── metric_definitions: [MetricDef]
│ └── measurements: [Measurement]
│
└── relationships
├── allocations: [Allocation]
└── updates: [Update]
Transaction Types
Incoming Transactions
Money coming into an organization:
IncomingTransaction
├── id: string (txn_xxx)
├── organization_id: string
├── type: enum
│ ├── donation
│ ├── grant
│ ├── transfer_in
│ ├── refund_received
│ └── interest
├── amount: integer (cents)
├── currency: string
├── status: enum (pending, completed, failed, refunded)
├── timestamp: timestamp
├── visibility: enum
│
├── source
│ ├── donor_id: string (if applicable)
│ ├── business_id: string (if applicable)
│ ├── campaign_id: string (if applicable)
│ └── external_ref: string
│
├── allocation
│ ├── fund_id: string
│ └── project_id: string (optional)
│
├── chain
│ ├── prev_hash: string
│ └── entry_hash: string
│
└── metadata
├── payment_method: string
├── stripe_payment_id: string
└── ...
Outgoing Transactions
Money spent by an organization:
OutgoingTransaction
├── id: string (txn_xxx)
├── organization_id: string
├── type: enum
│ ├── expense
│ ├── grant_out
│ ├── transfer_out
│ ├── refund_issued
│ └── fee
├── amount: integer (cents)
├── currency: string
├── status: enum
├── timestamp: timestamp
├── visibility: enum
│
├── categorization
│ ├── category: enum (operations, programs, fundraising, admin, ...)
│ ├── subcategory: string
│ └── sdgs: array[1-17]
│
├── source_fund
│ ├── fund_id: string
│ └── project_id: string (optional)
│
├── recipient
│ ├── type: enum (vendor, individual, organization, internal)
│ ├── name: string (may be aggregated)
│ └── external_id: string (if applicable)
│
├── chain
│ ├── prev_hash: string
│ └── entry_hash: string
│
└── metadata
├── description: string
├── invoice_ref: string
└── ...
User Entities
Data Minimization Principle
Amply collects only data necessary for platform functionality. We deliberately do not collect:
- Gender — No feature requires it; fails GDPR data minimization (Article 5(1)(c))
- Date of birth — Unless legally required for specific jurisdictions
- Demographic data — If needed for research, collected via optional surveys, not profiles
This reduces privacy liability, registration friction, and storage obligations while respecting user privacy.
Users
Individuals with accounts on Amply:
User
├── id: string (usr_xxx)
├── email: string
├── name: string
├── password_hash: string
├── status: enum (active, suspended)
├── created_at: timestamp
├── updated_at: timestamp
│
├── security
│ ├── security_stamp: string # Random token, rotated on password/security changes
│ ├── password_changed_at: timestamp
│ └── failed_login_attempts: integer
│
├── security_settings
│ ├── ip_binding: enum (none, country, subnet, strict)
│ ├── session_timeout_days: integer # Default 7
│ ├── require_2fa: boolean
│ └── notify_new_device: boolean # Default true
│
├── verification
│ ├── email_verified: boolean
│ ├── identity_verified: boolean
│ └── verification_date: timestamp
│
└── relationships
├── donor_profile: DonorProfile
├── organization_memberships: [OrgMembership]
├── business_roles: [BusinessRole]
└── fundraiser_campaigns: [Campaign]
Donors
Donor-specific profile:
DonorProfile
├── user_id: string
├── default_visibility: enum
├── communication_preferences: object
│
├── giving_history
│ ├── total_given: integer
│ ├── donations_count: integer
│ ├── organizations_supported: integer
│ └── recurring_donations: [RecurringDonation]
│
└── saved
├── favorite_orgs: [string]
├── payment_methods: [PaymentMethod]
└── giving_goals: [GivingGoal]
Campaign Entities
Campaigns
Fundraising campaigns:
Campaign
├── id: string (camp_xxx)
├── organization_id: string
├── fundraiser_id: string (if personal)
├── name: string
├── description: text
├── type: enum (organizational, personal, team, event)
├── status: enum (draft, active, completed, cancelled)
├── goal: integer
├── raised: integer
├── donor_count: integer
├── start_date: timestamp
├── end_date: timestamp
│
├── settings
│ ├── amounts: [integer]
│ ├── allow_recurring: boolean
│ ├── visibility: enum
│ └── fundraiser_enabled: boolean
│
└── relationships
├── donations: [Transaction]
├── fundraisers: [FundraiserLink]
└── updates: [CampaignUpdate]
Business Entities
Businesses
Companies using Amply:
Business
├── id: string (biz_xxx)
├── name: string
├── legal_name: string
├── type: enum (corporation, llc, partnership, sole_prop)
├── status: enum (pending, verified, suspended)
├── country: string
├── tax_id: string (encrypted)
│
├── capabilities
│ ├── corporate_giving: boolean
│ ├── customer_collections: boolean
│ └── employee_giving: boolean
│
├── integrations
│ ├── pos_type: string
│ ├── ecommerce_platform: string
│ └── api_keys: [APIKey]
│
└── relationships
├── team_members: [User]
├── supported_orgs: [Organization]
└── donations_made: [Transaction]
└── donations_collected: [Transaction]
Relational Structure
Entity Relationships
Organization ──┬── Funds ──┬── Projects
│ └── Transactions
├── Campaigns ── Donations
└── Team Members
Business ──┬── Corporate Donations ──▶ Organizations
└── Customer Collections ──▶ Organizations
Donor ──┬── Donations ──▶ Organizations
└── Campaigns (as fundraiser)
Fundraiser ── Campaign ──▶ Organization
└── Donations from others
Multi-Tenancy
Data isolation by organization:
Query context always includes organization_id
├── All transactions scoped
├── All funds/projects scoped
├── Cross-org only for explicit transfers
├── Global views require elevated access
└── Permission enforced via SpiceDB relationships
Ledger Integration
Transaction → Ledger Entry
Every transaction creates a ledger entry:
Transaction created
│
▼
Ledger entry generated
│
▼
Hash calculated (including prev_hash)
│
▼
Entry stored
│
▼
Materialized views updated
Denormalization
For performance, key data is denormalized:
Aggregates:
- Fund balances
- Campaign totals
- Organization metrics
- Donor totals
Audit Trail:
- All changes logged
- Original values preserved
- Aggregates reconcilable to entries
Multi-Currency Handling
Currency Strategy
Amply supports multiple currencies with per-fund isolation:
Principles:
- Each fund has a single currency (no mixing within a fund)
- Donations are recorded in the currency received
- No automatic currency conversion
- Amounts always stored in smallest unit (cents/pence)
Supported Currencies:
- EUR (Euro) - Primary for EU organizations
- GBP (British Pound)
- USD (US Dollar)
- CHF (Swiss Franc)
Multi-Currency Organizations
Organizations may have multiple funds in different currencies:
Organization (German nonprofit)
├── Fund: "General - EUR" (currency: EUR)
├── Fund: "UK Operations" (currency: GBP)
└── Fund: "US Donors" (currency: USD)
Each fund maintains its own ledger chain and balance in its currency.
Reporting
- Totals shown per-currency (no aggregation across currencies)
- Exchange rate data not stored (not Amply's function)
- Organizations responsible for their own FX conversions
- Ledger remains a record of actual transactions
Data Lifecycle
Retention
- Ledger entries: Permanent
- Transaction data: Permanent (legal requirements)
- User data: Retained while active + regulatory period
- Logs: 90 days operational, 7 years audit
Deletion
GDPR/privacy deletions:
- User PII can be anonymized
- Donations remain (amounts, timestamps)
- Attribution removed
- Ledger integrity maintained
Archival
Historical data:
- Moved to cold storage after 2 years
- Still accessible for verification
- Checkpoints remain active
- Audit capability preserved
Related: