Intranet Sync Design
This document explains the architecture and design rationale behind the intranet data synchronization system.
Problem
The Wippidu Kita App needs to receive organizational data (locations, groups, children, employees, relationships) from an external intranet management system. This data must be imported safely without disrupting the running application.
Two-phase architecture
The sync system uses a deliberate two-phase design:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Wippidu │ │ Wippidu App │ │ Wippidu App │
│ Intranet │─────►│ Staging Tables │─────►│ App Tables │
│ (External) │ API │ (sync_*) │Admin │ (locations, │
└─────────────────┘ └──────────────────┘ UI │ groups, etc.) │
└─────────────────┘
Phase 1 — Data Reception (API): The external intranet system pushes data to staging tables via authenticated REST API. This is fully automated.
Phase 2 — Data Processing (Admin): An admin reviews pending data and triggers processing, which transforms staging records into application tables. This is a deliberate manual step.
Why two phases?
- Safety: Data from external systems is untrusted. Staging tables act as a buffer, allowing admins to review before affecting the live application.
- Atomicity: Processing runs in dependency order (locations → groups → children → assignments). If one step fails, earlier steps are already committed and can be retried.
- Audit trail: All processing operations are logged with timestamps, counts, and the admin who triggered them.
- Decoupling: The API can accept data at any time without worrying about processing dependencies. Processing happens when the admin is ready.
Data flow detail
External Intranet System
│
│ HTTP POST + Bearer Token
▼
┌─────────────────────────────────────┐
│ API Authentication │
│ (middleware/api_token.go) │
│ • Validate Bearer token │
│ • Check IP allowlist │
│ • Update last_used_at │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ API Handlers │
│ (controller/intranet_api.go) │
│ • Parse JSON payload │
│ • Upsert to staging tables │
│ • Return sync result │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Staging Tables │
│ sync_persons, sync_groups, etc. │
└─────────────────────────────────────┘
│
│ Admin triggers processing
▼
┌─────────────────────────────────────┐
│ Processing Service │
│ (service/sync_processor.go) │
│ • Process in dependency order │
│ • Match by ExternalID │
│ • Create/update app records │
│ • Log results │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ App Tables │
│ locations, groups, children │
└─────────────────────────────────────┘
Processing dependency order
Processing must occur in this order because of foreign key relationships:
1. Locations (no dependencies)
│
▼
2. Groups (depends on Location via EinrichtungsID)
│
▼
3. Children (no dependencies, but logically after groups)
│
▼
4. Child Groups (depends on Child + Group)
ExternalID mapping
App tables use ExternalID fields to match staging records to existing app records. This enables upsert behavior:
- If a matching
ExternalIDexists → update the record - If no match → create a new record
This pattern is implemented in SyncProcessor for each entity type.
What gets processed vs. what doesn't
Directly processed (staging → app tables):
| Staging | App Table | Notes |
|---|---|---|
| sync_locations | locations | Direct mapping |
| sync_groups | groups | Requires location dependency |
| sync_persons (rolle=1) | children | Children only |
| sync_belegungen (status=3) | children.group_id | Updates group assignment |
Used by registration flow (not direct processing):
| Staging | Usage |
|---|---|
| sync_employees | Employee registration lookup |
| sync_persons (rolle=2,3,4) | Parent registration lookup |
| sync_parent_children | Link parent to children during registration |
| sync_location_leads | Assign LocationLead role during registration |
| sync_group_leads | Assign GroupLead role during registration |
Components
| Component | Location | Purpose |
|---|---|---|
| API Token Model | internal/model/sync.go |
Token authentication storage |
| Staging Models | internal/model/sync.go |
Staging table definitions |
| Auth Middleware | internal/middleware/api_token.go |
Bearer token + IP validation |
| API Controller | internal/controller/intranet_api.go |
Receive and store data |
| Processing Service | internal/service/sync_processor.go |
Transform staging → app tables |
| Admin Controller | internal/controller/admin_sync.go |
Trigger processing via UI |
| Routes | internal/route/intranet.go |
API endpoint definitions |
Daily employee group sync
In addition to the batch sync mechanism above, the app supports real-time daily group assignments for employees. This handles scenarios where employees substitute in different groups day-by-day.
How it works
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Employee │ │ Wippidu App │ │ Intranet │
│ Logs In │─────►│ (on login) │─────►│ Groups API │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │
│◄─── JSON response ───────┤
│ with group IDs │
▼
┌──────────────────┐
│ employee_daily │
│ _groups table │
└──────────────────┘
- Login trigger: When an intranet-linked employee logs in, the app checks if they've been refreshed today
- API call: If not, the app calls the intranet API with the employee's
Mitarbeiter_ID - Response processing: The API returns group assignments with
Stammteam(core team) flags - Priority logic: Substitute groups (
Stammteam=false) take precedence over core team groups - Storage: Assignments are stored in
employee_daily_groupswith today's date
Viewing vs. creating
The daily sync creates a deliberate separation between access levels:
| Permission | Source Function | Groups Included |
|---|---|---|
| View children | GetEmployeeGroups() |
All daily groups (substitute or core) |
| Create content | GetEmployeeCoreTeamGroups() |
Only core team groups |
This means: - A substitute can see children in their substitute group - A substitute cannot create news/letters/events for that group - Creation rights stay with their home (Stammteam) group
Why this design?
- Real-time accuracy: Staff schedules change daily; batch sync isn't fast enough
- Minimal permissions: Substitutes shouldn't modify content for groups they don't normally manage
- Audit clarity: Content is always created by permanent staff, not temporary substitutes
Related
- Intranet Sync API Reference — Endpoint documentation
- How to Sync Intranet Data — Practical guide