Skip to content

Arkitektur

🏗️ System Architecture

┌─────────────────────────────────────────────────────┐
│                  React Frontend (Vite)              │
│  ┌───────────────────────────────────────────────┐  │
│  │ Admin UI      │ Delegate UI │ Components       │  │
│  │ (AdminApp)    │ (ClientApp) │ (shared)         │  │
│  └───────────────────────────────────────────────┘  │
└──────────────────────┬──────────────────────────────┘
                       │
                    Firebase SDK
                       │
        ┌──────────────┼──────────────┐
        │              │              │
    ┌───▼──┐     ┌─────▼──┐   ┌──────▼──┐
    │Auth  │     │Firestore│   │Storage  │
    │      │     │Database │   │(Files)  │
    └──────┘     └─────────┘   └─────────┘
        │              │              │
    ┌───▼──────────────▼──────────────▼───┐
    │   Firebase Backend (europe-north1)   │
    │   - Real-time Firestore              │
    │   - Cloud Functions                  │
    │   - Security Rules                   │
    └─────────────────────────────────────┘

📦 Tech Stack

Frontend

  • React 19 - UI components
  • TypeScript 5.7 - Type safety
  • Vite 6.1 - Build tool & dev server
  • Tailwind CSS 3.4 - Styling
  • Lucide React - Icons

Backend

  • Firebase Firestore - Real-time document database
  • Firebase Auth - User authentication
  • Cloud Functions - Serverless compute (planned)
  • Cloud Storage - File storage

DevOps

  • GitHub - Version control
  • Firebase Hosting - Production deployment
  • GitHub Actions - CI/CD (planned)

🗄️ Firestore Data Model

Collections

members

{
  id: string (Golf-ID)
  name: string
  email: string
  phone: string
  role: string
  verification: string (post code for login)
  canVote: boolean
  joinDate: timestamp
}

agenda

{
  id: string
  number: number (§1, §2, etc)
  title: string
  description: string
  status: 'PENDING' | 'ACTIVE' | 'VOTING_OPEN' | 'VOTING_CLOSED' | 'FINISHED'
  isVotable: boolean
  requiresSecretBallot: boolean
  decision?: string
  proposals?: Proposal[]
  voteResults?: { for, against, abstain, total }
}

speakers

{
  id: string
  agendaItemId: string
  userId: string
  userName: string
  type: 'INNLEGG' | 'REPLIKK'
  timestamp: timestamp
}

votes (Anonymous, never readable)

{
  id: string
  agendaItemId: string
  voteOption: 'FOR' | 'MOT' | 'AVSTAR'
  timestamp: timestamp
}

vote_receipts (Proof of participation)

{
  id: string (userId)
  agendaItemId: string
  hasVoted: boolean
  timestamp: timestamp
}

🔐 Security Model

Firestore Rules

  • Members: Read-only own data (no client writes)
  • Agenda: Authenticated read, admin-only write
  • Speakers: Authenticated create, real-time read
  • Votes: Anonymous creation, never readable
  • Default: Deny-all for unspecified paths

Authentication

  • Golf ID (6 digits) + PIN (4 digits) verification
  • No OAuth - direct Firestore lookup

🔄 Data Flow

Login Flow (Delegate)

1. User enters Golf ID + PIN in ClientApp
2. Query Firestore: members/{golfId}
3. Verify PIN matches member.verification
4. Check member.canVote permission
5. Set isCheckedIn = true
6. Subscribe to real-time agenda

Voting Flow (Planned)

1. Admin opens voting (toggleVoting in ConductorView)
2. Update agenda.status = 'VOTING_OPEN'
3. Delegate casts vote → Cloud Function
4. Function creates anonymous votes/{voteId}
5. Function creates vote_receipts/{userId}
6. Real-time aggregation shows vote count
7. Admin closes voting → status = 'VOTING_CLOSED'

📁 Component Hierarchy

App.tsx
├── AdminApp.tsx (role-based)
│   ├── ConductorView (§-behandling)
│   └── SecretaryView (protokoll)
├── ClientApp.tsx (mobile-first)
│   ├── DashboardView
│   ├── MemberListView
│   ├── UploadView (secretary)
│   └── AIView (Gemini insights)
└── Shared Components
    ├── Sidebar
    ├── DashboardView
    └── MemberListView

🎯 Real-time Subscriptions

All components use custom hooks for data binding:

// Collection listener
const { data: agenda, loading, error } = useCollection('agenda');

// Document listener
const { data: activeItem, exists } = useDocument('agenda', itemId);

Auto-unsubscribe on unmount, handles loading/error states.