docs: add phase 3 design polish planning, update progress

This commit is contained in:
2026-02-26 23:53:22 +01:00
parent 03c76cb316
commit 3493ffdf44
12 changed files with 2695 additions and 22 deletions
+4
View File
@@ -15,6 +15,10 @@ Research sammanställd 2026-02-15 via Exa AI Search.
| [07-recommendations.md](07-recommendations.md) | Konkreta rekommendationer för Gravl |
| [08-sources.md](08-sources.md) | Alla källor och länkar |
| [09-exercise-databases.md](09-exercise-databases.md) | Övningsdatabaser, APIs, media, substitution |
| [10-onboarding-retention.md](10-onboarding-retention.md) | Onboarding flows, retention strategies, push notifications |
| [11-progressive-overload.md](11-progressive-overload.md) | Progressionsalgoritmer, RPE/RIR, 1RM-beräkning |
| [12-offline-first.md](12-offline-first.md) | Offline-first arkitektur, sync strategies |
| [13-monetization.md](13-monetization.md) | Freemium, subscription, pricing psychology |
## Key Takeaways
@@ -0,0 +1,420 @@
# Onboarding & Retention — Research för Gravl
## Problemet
> "70% of fitness app users drop off within the first 90 days. The reason isn't a lack of motivation. It's bad UX."
> "80% of New Year's resolutions fail by February"
**Retention-statistik:**
- Day 1: ~25% retention (average app)
- Day 7: ~15% retention
- Day 30: ~5-10% retention
- Fitness apps: Ofta ännu sämre pga motivation-dependent
---
## Del 1: Onboarding
### Varför onboarding är kritiskt
> "First impressions matter. For mobile apps, onboarding is the moment of truth — the experience that determines whether a new user becomes engaged or churns within minutes."
### Onboarding Goals
1. **Visa värde snabbt** — "Aha moment" inom 60 sekunder
2. **Samla nödvändig data** — Men inte mer än nödvändigt
3. **Personalisera upplevelsen** — Anpassa till användaren
4. **Skapa första framgången** — Quick win
5. **Bygga vana** — Första steget mot retention
### Onboarding-typer
| Typ | Beskrivning | Best for |
|-----|-------------|----------|
| **Progressive** | Gradvis introduktion | Komplexa appar |
| **Benefits-oriented** | Visa värde först | Skeptiska användare |
| **Function-oriented** | Lär ut features | Verktygs-appar |
| **Account-focused** | Registrering först | Community-appar |
| **Conversational** | Dialog-baserad | Personaliserade appar |
### Conversational Onboarding (Rekommenderat för Gravl)
**Traditionellt:**
```
Screen 1: Välj mål [Styrka] [Hypertrofi] [Fettförbränning]
Screen 2: Välj erfarenhet [Nybörjare] [Medel] [Avancerad]
Screen 3: Välj dagar [1] [2] [3] [4] [5] [6] [7]
Screen 4: Ange vikt [____ kg]
```
**Conversational:**
```
Coach: "Hej! Jag är din träningscoach. Vad vill du uppnå?"
User: "Jag vill bli starkare och se bättre ut"
Coach: "Bra mål! Hur länge har du tränat?"
User: "Typ ett år, men ganska sporadiskt"
Coach: "Ok, du har en bra grund! Hur många dagar per vecka
kan du verkligen träna, realistiskt?"
User: "3-4 dagar"
Coach: "Perfekt för PPL! En sista sak — hur mycket väger du
ungefär? Det hjälper mig sätta rätt startvikter."
User: "85 kg"
Coach: "Toppen! Jag har skapat ett program för dig. Redo att
köra ditt första pass?"
```
**Fördelar:**
- Känns personligt, inte som ett formulär
- Samlar mer context ("ganska sporadiskt")
- Användaren känner sig hörd
- Naturlig felhantering
### Onboarding Best Practices
#### 1. Minimera friktion
```
❌ 8 steg, 15 frågor, email-verifiering
✅ 3-4 steg, 5-7 frågor, skip email
```
#### 2. Visa värde INNAN du ber om data
```
❌ "Registrera dig för att fortsätta"
✅ "Här är ditt första pass!" → "Spara din progress?"
```
#### 3. Progressive disclosure
```
Steg 1: Grundläggande (mål, erfarenhet)
Steg 2: Senare (kroppsmått, 1RM)
Steg 3: Över tid (preferenser, historik)
```
#### 4. Default-värden
```
❌ "Ange din 1RM på bänkpress: [____]"
✅ "Din estimerade 1RM: [60kg] (baserat på erfarenhet)"
```
#### 5. Instant gratification
```
Onboarding → Första passet → Completion celebration
(helst inom 5-10 minuter)
```
### Onboarding Metrics
| Metric | Mål | Beskrivning |
|--------|-----|-------------|
| **Completion rate** | >80% | Andel som avslutar onboarding |
| **Time to value** | <2 min | Tid till första "aha moment" |
| **Drop-off points** | Identify | Var lämnar användare? |
| **Day 1 activation** | >50% | Andel som gör första passet |
---
## Del 2: Retention
### Retention Strategies (13 från Orangesoft)
#### 1. Personalisering
> "47% of users say they'd leave apps that don't personalize their experience"
- Anpassade program baserat på mål
- Dynamiskt innehåll baserat på beteende
- Personliga hälsningar
#### 2. Gamification
- Streaks och achievements
- Progress visualization
- Leaderboards (opt-in)
#### 3. Social features
- Workout sharing
- Challenges med vänner
- Community support
#### 4. Push notifications
- Workout reminders
- Streak warnings
- Achievement celebrations
#### 5. Goal tracking
- Visuell progress
- Milestones
- Before/after comparisons
#### 6. Content variety
- Nya övningar regelbundet
- Seasonal challenges
- Expert tips
#### 7. Wearable integration
- Apple Watch
- Garmin, Fitbit
- Auto-sync
#### 8. AI coaching
- Adaptiva program
- Form feedback
- Recovery recommendations
#### 9. Offline functionality
- Fungerar utan internet
- Sync när online
#### 10. Feedback loops
- Rate your workout
- Adjust difficulty
- Learn preferences
#### 11. Community
- Forums/comments
- User-generated content
- Social accountability
#### 12. Rewards
- Badges/achievements
- Discounts/perks
- Real rewards
#### 13. Seamless UX
- Fast load times
- Intuitive navigation
- Consistent design
### Habit Formation
#### "21 Days" är en myt
> "The popular belief that it takes 21 days to form a habit is actually a myth."
**Verkligheten:**
- 18-254 dagar beroende på beteende
- Genomsnitt: ~66 dagar
- Enklare habits = snabbare (vatten)
- Svårare habits = längre (gym)
#### Habit Loop (från "Hooked")
```
┌─────────────────────────────────────┐
│ │
▼ │
┌───────┐ ┌────────┐ ┌────────┐ │
│ CUE │───▶│ ACTION │───▶│ REWARD │────┘
└───────┘ └────────┘ └────────┘
```
**Fitness app-tillämpning:**
1. **Cue:** Push notification, tid på dagen, location
2. **Action:** Öppna app, starta pass
3. **Reward:** Progress, achievement, dopamine
#### Fabulous App (Google Design Award)
> "Leveraging Material Design guidelines, the company created an engaging UI around science-based strategies for psychological reinforcement, motivating users from onboarding through goal completion."
**Resultat:** 16x ökning i dagliga downloads
---
## Del 3: Push Notifications
### Statistik
- Push kan öka engagement med **80%**
- Push kan öka retention med **88%**
- Men **53%** tycker push är irriterande
### Timing (Fitness Apps)
| Tid | Typ | Varför |
|-----|-----|--------|
| **7-9 AM** | Morgon-workout reminder | Innan dagen startar |
| **5-7 PM** | Kvälls-workout reminder | Efter jobb |
| **8-9 PM** | Achievement summary | Reflektera över dagen |
| **Söndag kväll** | Weekly summary | Prep för veckan |
### Fitness-specifika Push-strategier
#### 1. Workout Reminders
```
🏋️ "Dags för Pull-dag! Redo att krossa det?"
[Starta pass] [Påminn senare]
```
#### 2. Streak Warnings
```
🔥 "Din 7-dagars streak är i fara! Logga ett pass idag."
```
#### 3. Achievement Celebrations
```
🎉 "NYTT PR! 100kg bänkpress! Du är starkare än 78% av användarna."
```
#### 4. Progress Updates
```
📈 "Förra veckan: 4 pass, 12,500 kg totalt. +8% vs förra veckan!"
```
#### 5. Re-engagement
```
😢 "Vi saknar dig! Ditt senaste pass var för 5 dagar sedan."
```
### Push Best Practices
#### DO:
✅ Personalisera (namn, mål, historik)
✅ Skicka vid rätt tid (user timezone)
✅ Ge värde (tips, achievements, progress)
✅ A/B-testa copy
✅ Respektera quiet hours
✅ Låt användare välja frekvens
#### DON'T:
❌ Spamma (max 1-2/dag)
❌ Generiska meddelanden
❌ Skicka mitt i natten
❌ Ignorera opt-outs
❌ Samma meddelande varje dag
### Push Notification Triggers
```python
def should_send_push(user):
# Reminder for scheduled workout
if user.has_workout_today and not user.started_workout:
if is_optimal_time(user):
return "workout_reminder"
# Streak at risk
if user.streak > 3 and user.days_since_workout == 1:
return "streak_warning"
# Achievement unlocked
if user.new_achievements:
return "achievement"
# Re-engagement
if user.days_since_workout >= 5:
return "re_engagement"
return None
```
---
## Del 4: Rekommendationer för Gravl
### Onboarding Flow
```
1. Welcome Screen (5s)
"Hej! Redo att bli starkare?"
[Kom igång]
2. Goal Selection (conversational)
Coach: "Vad vill du uppnå?"
[Styrka] [Muskler] [Gå ner i vikt] [Allmän fitness]
3. Experience Level
Coach: "Hur länge har du tränat?"
[Nybörjare] [6-12 månader] [1-3 år] [3+ år]
4. Schedule
Coach: "Hur många dagar per vecka kan du träna?"
[2] [3] [4] [5] [6]
5. Quick Profile (optional)
Coach: "Vikt hjälper mig sätta rätt startvikter"
[____ kg] eller [Hoppa över]
6. Program Generated
"Ditt PPL-program är klart! Första passet: Push A"
[Starta nu] [Senare]
```
**Total tid:** ~90 sekunder
### Retention Checklist
#### Week 1: Activation
- [ ] Första passet genomfört
- [ ] Första PR celebration
- [ ] Push notification opt-in
- [ ] Förklara streak-systemet
#### Week 2-4: Habit Building
- [ ] 3+ pass/vecka
- [ ] Streak etablerad
- [ ] Första achievement unlocked
- [ ] Progress-graf visar förbättring
#### Month 2+: Long-term Retention
- [ ] Program-byte erbjuds
- [ ] Milestones firande (50 pass, etc.)
- [ ] Referral program
- [ ] Advanced features unlock
### Key Metrics att Tracka
| Metric | Target | When to Measure |
|--------|--------|-----------------|
| Onboarding completion | >80% | Immediate |
| Day 1 activation | >50% | Day 1 |
| Day 7 retention | >30% | Day 7 |
| Day 30 retention | >20% | Day 30 |
| Weekly active users | — | Ongoing |
| Workouts/week/user | >2.5 | Ongoing |
---
## Källor
- UXCam, CleverTap, Sendbird — Onboarding examples
- Orangesoft, Stormotion — Retention strategies
- Braze, Pushwoosh — Push notification best practices
- ContextSDK — Timing optimization
- Google Design (Fabulous) — Behavior change
- PMC — Habit formation research
- Octalysis Group — Gamification framework
---
*Sammanställt 2026-02-15 av Bumblebee 🐝*
@@ -0,0 +1,517 @@
# Progressive Overload-algoritmer — Research för Gravl
## Vad är Progressive Overload?
> "Progressive overload is the gradual increase of stress placed on the body during training. To continue building strength and muscle, you must progressively increase the demands on your musculoskeletal system."
**Grundprincipen:** Om du gör samma träning med samma vikter, reps och sets vecka efter vecka har kroppen ingen anledning att anpassa sig.
---
## Progressionsmetoder
### 1. Vikt-progression (Linear)
**Enklast och mest effektiv för nybörjare/intermediates**
```
Vecka 1: Bänkpress 60kg x 8,8,8
Vecka 2: Bänkpress 62.5kg x 8,8,8
Vecka 3: Bänkpress 65kg x 8,8,8
...
```
**Typiska ökningar:**
| Övning | Ökning per pass |
|--------|-----------------|
| Squat/Deadlift | +2.5-5 kg |
| Bench/Row/OHP | +1.25-2.5 kg |
| Isolation (curls, etc.) | +1-2 kg |
### 2. Rep-progression (Double Progression)
**När du inte kan öka vikt varje vecka**
```
Mål: 3x8-12 reps
Vecka 1: 60kg x 8,8,8 (låg end)
Vecka 2: 60kg x 9,9,8
Vecka 3: 60kg x 10,10,10
Vecka 4: 60kg x 12,11,11
Vecka 5: 62.5kg x 8,8,8 (öka vikt, börja om)
```
**Regel:** Öka vikt när alla sets når övre rep-gränsen.
### 3. Set-progression
```
Vecka 1: 60kg x 8,8,8 (3 sets)
Vecka 2: 60kg x 8,8,8,8 (4 sets)
Vecka 3: 62.5kg x 8,8,8 (tillbaka till 3 sets, ny vikt)
```
### 4. RPE/RIR-baserad Autoregulation
**RPE = Rate of Perceived Exertion (1-10)**
**RIR = Reps in Reserve**
| RPE | RIR | Beskrivning |
|-----|-----|-------------|
| 10 | 0 | Failure (kunde inte gjort fler) |
| 9.5 | 0.5 | Kanske 1 till med dålig form |
| 9 | 1 | 1 rep kvar |
| 8.5 | 1.5 | 1-2 reps kvar |
| 8 | 2 | 2 reps kvar |
| 7 | 3 | 3 reps kvar |
| 6 | 4 | Uppvärmning |
**Konvertering:** `RPE = 10 - RIR`
**Användning:**
```
Målsättning: 3x8 @ RPE 8
Set 1: 80kg x 8 @ RPE 7 → för lätt, öka
Set 2: 82.5kg x 8 @ RPE 8 → perfekt
Set 3: 82.5kg x 8 @ RPE 9 → trötthet, behåll vikt
```
---
## 1RM-beräkning
### Populära formler
#### Epley Formula (mest använd)
```
1RM = weight × (1 + reps/30)
```
**Exempel:** 80kg × 10 reps
```
1RM = 80 × (1 + 10/30) = 80 × 1.333 = 106.7 kg
```
#### Brzycki Formula
```
1RM = weight × (36 / (37 - reps))
```
**Exempel:** 80kg × 10 reps
```
1RM = 80 × (36 / (37 - 10)) = 80 × 1.333 = 106.7 kg
```
#### Lander Formula
```
1RM = weight × (100 / (101.3 - 2.67 × reps))
```
### Rep Max Tabell (% av 1RM)
| Reps | % av 1RM | Vikt (om 1RM = 100kg) |
|------|----------|----------------------|
| 1 | 100% | 100 kg |
| 2 | 94% | 94 kg |
| 3 | 91% | 91 kg |
| 4 | 88% | 88 kg |
| 5 | 86% | 86 kg |
| 6 | 83% | 83 kg |
| 7 | 81% | 81 kg |
| 8 | 79% | 79 kg |
| 9 | 77% | 77 kg |
| 10 | 75% | 75 kg |
| 12 | 70% | 70 kg |
| 15 | 65% | 65 kg |
---
## Progressionsalgoritmer för Gravl
### Algoritm 1: Simple Linear (Nybörjare)
```python
def calculate_next_weight(exercise, last_workout):
"""
Enkel linjär progression.
Om alla sets klarades → öka vikt.
"""
target_reps = exercise.target_reps # ex: 8
achieved_reps = last_workout.reps # ex: [8, 8, 8]
# Alla sets klarade?
if all(r >= target_reps for r in achieved_reps):
increment = get_increment(exercise.type)
return last_workout.weight + increment
else:
return last_workout.weight # Repetera samma vikt
def get_increment(exercise_type):
"""Standardökningar baserat på övningstyp."""
increments = {
'compound_lower': 2.5, # Squat, Deadlift
'compound_upper': 1.25, # Bench, OHP, Row
'isolation': 1.0, # Curls, Extensions
}
return increments.get(exercise_type, 1.25)
```
### Algoritm 2: Double Progression (Rep Range)
```python
def calculate_next_weight_double(exercise, last_workout):
"""
Double progression med rep range (ex: 8-12 reps).
Öka vikt när alla sets når övre gränsen.
"""
min_reps = exercise.min_reps # ex: 8
max_reps = exercise.max_reps # ex: 12
achieved_reps = last_workout.reps
# Alla sets på max reps?
if all(r >= max_reps for r in achieved_reps):
increment = get_increment(exercise.type)
return {
'weight': last_workout.weight + increment,
'target_reps': min_reps # Börja om på min_reps
}
# Alla sets klarade min_reps?
elif all(r >= min_reps for r in achieved_reps):
return {
'weight': last_workout.weight,
'target_reps': min(max(achieved_reps) + 1, max_reps)
}
else:
# Missade reps, behåll allt
return {
'weight': last_workout.weight,
'target_reps': min_reps
}
```
### Algoritm 3: RPE-baserad Autoregulation
```python
def calculate_next_weight_rpe(exercise, last_workout):
"""
RPE-baserad progression.
Justerar vikt baserat på hur hårt det kändes.
"""
target_rpe = exercise.target_rpe # ex: 8
achieved_rpe = last_workout.rpe # ex: [7, 8, 9]
avg_rpe = sum(achieved_rpe) / len(achieved_rpe)
# Under target RPE → för lätt, öka
if avg_rpe < target_rpe - 0.5:
adjustment = (target_rpe - avg_rpe) * 2.5 # ~2.5kg per RPE
return last_workout.weight + adjustment
# Över target RPE → för tungt, minska
elif avg_rpe > target_rpe + 0.5:
adjustment = (avg_rpe - target_rpe) * 2.5
return last_workout.weight - adjustment
# Inom range → perfekt, små ökning
else:
return last_workout.weight + get_increment(exercise.type)
```
### Algoritm 4: Hybrid (Gravl Recommendation)
```python
def calculate_progression(exercise, history, user):
"""
Hybrid-algoritm som kombinerar flera metoder.
1. Nybörjare: Linear progression
2. Intermediate: Double progression
3. Avancerad: RPE-baserad
Med säkerhetschecks och platå-hantering.
"""
last_workout = history[-1] if history else None
if not last_workout:
return estimate_starting_weight(exercise, user)
# Välj metod baserat på erfarenhet
if user.experience == 'beginner':
return linear_progression(exercise, last_workout)
elif user.experience == 'intermediate':
return double_progression(exercise, last_workout)
else:
return rpe_progression(exercise, last_workout)
def estimate_starting_weight(exercise, user):
"""
Estimera startvikt för ny användare.
Baserat på kroppsvikt och erfarenhet.
"""
bodyweight = user.weight_kg
# Typiska ratio för 1RM baserat på erfarenhet
ratios = {
'beginner': {
'squat': 0.5,
'bench': 0.4,
'deadlift': 0.6,
'ohp': 0.25,
'row': 0.35,
},
'intermediate': {
'squat': 1.0,
'bench': 0.75,
'deadlift': 1.25,
'ohp': 0.5,
'row': 0.6,
}
}
ratio = ratios.get(user.experience, ratios['beginner'])
estimated_1rm = bodyweight * ratio.get(exercise.base_type, 0.5)
# Börja på ~65% av estimated 1RM (för 10 reps)
starting_weight = estimated_1rm * 0.65
# Avrunda till närmaste 2.5kg
return round(starting_weight / 2.5) * 2.5
```
---
## Platå-hantering
### Detektera platå
```python
def detect_plateau(history, window=4):
"""
Platå = ingen progress under [window] pass.
"""
if len(history) < window:
return False
recent = history[-window:]
weights = [w.weight for w in recent]
# Ingen viktökning?
if max(weights) <= min(weights):
# Kolla även reps
total_reps = [sum(w.reps) for w in recent]
if max(total_reps) <= min(total_reps):
return True
return False
```
### Platå-strategier
```python
def handle_plateau(exercise, history, strategy='deload'):
"""
Hantera platå med olika strategier.
"""
last_weight = history[-1].weight
if strategy == 'deload':
# Sänk vikt med 10-15%, bygg upp igen
return {
'weight': last_weight * 0.85,
'reason': 'Deload: Sänker vikt för att bygga upp igen'
}
elif strategy == 'rep_change':
# Byt rep-range (ex: 5x5 → 3x8)
return {
'weight': last_weight * 0.9,
'reps': 8,
'sets': 3,
'reason': 'Ny rep-range för att bryta platå'
}
elif strategy == 'exercise_swap':
# Byt övning temporärt
alternatives = get_alternatives(exercise)
return {
'exercise': alternatives[0],
'reason': 'Byter övning för variation'
}
```
---
## Deload-strategier
### Vad är Deload?
En planerad period med reducerad intensitet för recovery.
### Typer av Deload
| Typ | Vikt | Volym | När |
|-----|------|-------|-----|
| **Light Deload** | -10% | Same | Var 4:e vecka |
| **Volume Deload** | Same | -40% | Vid trött |
| **Full Deload** | -20% | -50% | Efter tuffa block |
### Automatisk Deload
```python
def should_deload(user, history):
"""
Avgör om deload behövs.
"""
weeks_since_deload = user.weeks_since_deload
# Schemalagd deload var 4-6 vecka
if weeks_since_deload >= 5:
return True
# RPE konsekvent hög
recent_rpe = [h.avg_rpe for h in history[-4:]]
if len(recent_rpe) >= 4 and all(r >= 9 for r in recent_rpe):
return True
# Missade reps ökar
recent_misses = count_missed_reps(history[-4:])
if recent_misses > 5:
return True
return False
```
---
## UX för Progression
### Visa progression transparent
```
┌────────────────────────────────────────────────┐
│ Bänkpress Nästa: 85kg │
├────────────────────────────────────────────────┤
│ │
│ Förra passet: 82.5kg x 8, 8, 8 │
│ Alla sets klarade! → Ökar med 2.5kg │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ [Progressionsgraf senaste 8 veckor] │ │
│ │ 85 ─ ● │ │
│ │ 80 ─ ● ● │ │
│ │ 75 ─ ● ● │ │
│ │ 70 ─ ● ● │ │
│ │ W1 W2 W3 W4 W5 W6 W7 W8 │ │
│ └──────────────────────────────────────────┘ │
│ │
│ [Godkänn 85kg] [Justera manuellt] │
└────────────────────────────────────────────────┘
```
### Förklara logiken
```
💡 Varför ökar vikten?
───────────────────────
Du tog 82.5kg x 8, 8, 8 förra passet.
Mål var 8-10 reps.
→ Alla sets klarade → Dags att öka!
→ +2.5kg är standard för överkropps-compound.
```
---
## Implementation för Gravl
### Database Schema
```sql
CREATE TABLE progression_settings (
id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(id),
exercise_id INT REFERENCES exercises(id),
-- Progression method
method VARCHAR(20) DEFAULT 'double', -- 'linear', 'double', 'rpe'
-- Rep range
min_reps INT DEFAULT 8,
max_reps INT DEFAULT 12,
target_sets INT DEFAULT 3,
-- Increments
weight_increment DECIMAL(4,2) DEFAULT 2.5,
-- Deload settings
deload_frequency_weeks INT DEFAULT 5,
deload_percentage DECIMAL(3,2) DEFAULT 0.85,
-- RPE settings
target_rpe DECIMAL(3,1) DEFAULT 8.0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE progression_history (
id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(id),
exercise_id INT REFERENCES exercises(id),
workout_id INT REFERENCES workouts(id),
weight DECIMAL(6,2),
reps INT[],
rpe DECIMAL(3,1)[],
-- Computed
estimated_1rm DECIMAL(6,2),
total_volume DECIMAL(10,2), -- weight × total_reps
performed_at TIMESTAMPTZ DEFAULT NOW()
);
```
### API Endpoint
```python
@app.get("/api/exercises/{exercise_id}/next-weight")
def get_next_weight(exercise_id: int, user: User):
"""
Returnerar nästa rekommenderade vikt för en övning.
"""
history = get_exercise_history(user.id, exercise_id)
settings = get_progression_settings(user.id, exercise_id)
next_weight = calculate_progression(
exercise=get_exercise(exercise_id),
history=history,
settings=settings,
user=user
)
return {
"exercise_id": exercise_id,
"recommended_weight": next_weight.weight,
"recommended_reps": next_weight.reps,
"reason": next_weight.reason,
"previous": history[-1] if history else None,
"progression_graph": get_progression_graph(history)
}
```
---
## Källor
- Setgraph, Zing Coach, FitnessAI — Progressive overload calculators
- JEFIT, RippedBody — RPE/RIR guides
- Stronglifts — Increment settings
- NASM, VBTCoach — 1RM formulas
- Alpha Progression, StrengthLog — Rep max tables
---
*Sammanställt 2026-02-15 av Bumblebee 🐝*
+553
View File
@@ -0,0 +1,553 @@
# Offline-First Implementation — Research för Gravl
## Varför Offline-First?
> "Mobile networks are unreliable. Users face data limits, weak signals, airplane mode, subway tunnels."
**Gym-specifikt:**
- Gym har ofta dålig/ingen WiFi
- Källare, betong, metall = dålig signal
- Användare vill inte vänta på laddning mellan sets
- Data får INTE förloras (loggade reps är värdefulla)
---
## Offline-First Principer
### Core Principles (från OneUptime)
1. **Local-first:** Data sparas lokalt FÖRST, synkas SEN
2. **Optimistic Updates:** UI uppdateras direkt, backend i bakgrund
3. **Graceful Degradation:** Features som kräver nätverk degraderas snyggt
4. **Conflict Resolution:** Tydlig strategi för datakonflikt
5. **Transparent Sync:** Användaren förstår sync-status
### Mental Model
```
┌─────────────────────────────────────────────────────────┐
│ USER ACTION │
│ (logga set) │
└─────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ LOCAL DATABASE │
│ (SQLite/IndexedDB) │
│ │
│ ✅ Omedelbar respons │
│ ✅ Fungerar offline │
│ ✅ Data säker lokalt │
└─────────────────────┬───────────────────────────────────┘
│ (när nätverk finns)
┌─────────────────────────────────────────────────────────┐
│ SYNC ENGINE │
│ │
│ • Queue pending changes │
│ • Retry on failure │
│ • Resolve conflicts │
└─────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ REMOTE SERVER │
│ (PostgreSQL API) │
└─────────────────────────────────────────────────────────┘
```
---
## Tekniska Alternativ
### 1. React Native + SQLite
**Bibliotek:** `react-native-sqlite-storage` eller `expo-sqlite`
**Fördelar:**
- Native performance
- Full SQL-support
- Beprövad teknologi
**Nackdelar:**
- Kräver native build
- Ingen inbyggd sync
```javascript
import * as SQLite from 'expo-sqlite';
const db = SQLite.openDatabase('gravl.db');
// Skapa tabell
db.transaction(tx => {
tx.executeSql(
`CREATE TABLE IF NOT EXISTS workout_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
exercise_id INTEGER,
weight REAL,
reps TEXT,
synced INTEGER DEFAULT 0,
local_id TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)`
);
});
// Logga set (offline-first)
const logSet = async (exerciseId, weight, reps) => {
const localId = uuid.v4();
// Spara lokalt FÖRST
db.transaction(tx => {
tx.executeSql(
'INSERT INTO workout_logs (exercise_id, weight, reps, local_id) VALUES (?, ?, ?, ?)',
[exerciseId, weight, JSON.stringify(reps), localId]
);
});
// Försök synka i bakgrund
syncToServer(localId);
};
```
### 2. React Native + RxDB
**RxDB:** Reactive Database med inbyggd sync
**Fördelar:**
- Reaktiv (observables)
- Inbyggd sync (CouchDB-protokoll)
- Conflict resolution
- TypeScript-stöd
**Nackdelar:**
- Mer komplex setup
- Större bundle
```javascript
import { createRxDatabase, addRxPlugin } from 'rxdb';
import { getRxStorageDexie } from 'rxdb/plugins/storage-dexie';
import { RxDBReplicationCouchDBPlugin } from 'rxdb/plugins/replication-couchdb';
addRxPlugin(RxDBReplicationCouchDBPlugin);
const db = await createRxDatabase({
name: 'gravldb',
storage: getRxStorageDexie()
});
// Schema
await db.addCollections({
workouts: {
schema: {
version: 0,
primaryKey: 'id',
properties: {
id: { type: 'string' },
exercise_id: { type: 'number' },
weight: { type: 'number' },
reps: { type: 'array' },
timestamp: { type: 'string' }
}
}
}
});
// Replication
const replicationState = db.workouts.syncCouchDB({
remote: 'https://api.gravl.app/sync',
push: { batchSize: 10 },
pull: { batchSize: 10 }
});
```
### 3. PWA + IndexedDB + Service Worker
**För web-first approach**
**Fördelar:**
- Ingen app store
- Fungerar på alla plattformar
- Service Worker caching
**Nackdelar:**
- Begränsad native-access
- iOS PWA-begränsningar
```javascript
// Service Worker (sw.js)
const CACHE_NAME = 'gravl-v1';
const OFFLINE_URLS = [
'/',
'/app.js',
'/styles.css',
'/exercises.json'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll(OFFLINE_URLS);
})
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cached => {
// Returnera cached först, hämta nytt i bakgrund
const networkFetch = fetch(event.request).then(response => {
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, response.clone());
});
return response;
});
return cached || networkFetch;
})
);
});
```
```javascript
// IndexedDB wrapper (Dexie)
import Dexie from 'dexie';
const db = new Dexie('GravlDB');
db.version(1).stores({
workouts: '++id, date, synced',
exercises: 'id, name, bodyPart',
pendingSync: '++id, type, data, timestamp'
});
// Offline-first save
async function saveWorkout(workout) {
// Spara lokalt
const id = await db.workouts.add({
...workout,
synced: false,
localId: crypto.randomUUID()
});
// Queue för sync
await db.pendingSync.add({
type: 'workout',
data: workout,
timestamp: Date.now()
});
// Trigger background sync
if ('serviceWorker' in navigator && 'sync' in registration) {
registration.sync.register('sync-workouts');
}
return id;
}
```
### 4. SQLite Sync (CRDT)
**Nytt:** SQLite Cloud's SQLite Sync extension
**Fördelar:**
- Äkta local-first
- CRDT för konfliktfri sync
- Standard SQLite API
```javascript
// SQLite Sync (konceptuell)
import { SQLiteSync } from 'sqlite-sync';
const db = new SQLiteSync('gravl.db', {
remote: 'https://sync.gravl.app',
tables: ['workouts', 'exercises']
});
// Automatisk sync!
await db.exec(`
INSERT INTO workouts (exercise_id, weight, reps)
VALUES (1, 80, '[8, 8, 8]')
`);
// Synkas automatiskt när online
```
---
## Sync Strategies
### 1. Optimistic UI
```javascript
// Användaren ser ändringen DIREKT
const logSet = async (data) => {
// 1. Uppdatera UI omedelbart
setWorkoutLogs(prev => [...prev, data]);
// 2. Spara lokalt
await localDB.save(data);
// 3. Synka i bakgrund (utan att blockera UI)
syncInBackground(data).catch(err => {
// Visa synkfel-indikator, men behåll data
showSyncError();
});
};
```
### 2. Conflict Resolution
**Strategier:**
| Strategi | Beskrivning | Bäst för |
|----------|-------------|----------|
| **Last Write Wins** | Senaste timestamp vinner | Enkel data |
| **Client Wins** | Lokal data prioriteras | User-kontroll |
| **Server Wins** | Server-data prioriteras | Data integrity |
| **Merge** | Kombinera ändringar | Komplex data |
| **CRDT** | Konfliktfri automatisk | Multi-device |
**Gravl-rekommendation:** Last Write Wins med server-timestamp
```javascript
const resolveConflict = (local, remote) => {
// Om samma workout redigerats på två enheter
if (local.updated_at > remote.updated_at) {
return local; // Nyare vinner
} else {
return remote;
}
};
```
### 3. Background Sync
```javascript
// Service Worker background sync
self.addEventListener('sync', event => {
if (event.tag === 'sync-workouts') {
event.waitUntil(syncPendingWorkouts());
}
});
async function syncPendingWorkouts() {
const pending = await db.pendingSync
.where('type')
.equals('workout')
.toArray();
for (const item of pending) {
try {
await fetch('/api/workouts', {
method: 'POST',
body: JSON.stringify(item.data)
});
// Ta bort från queue
await db.pendingSync.delete(item.id);
// Markera som synkad
await db.workouts
.where('localId')
.equals(item.data.localId)
.modify({ synced: true });
} catch (err) {
// Retry later
console.log('Sync failed, will retry');
}
}
}
```
---
## Sync Status UI
### Indikera sync-status
```jsx
// Sync-indikator komponent
const SyncStatus = () => {
const { pendingCount, lastSync, isOnline } = useSyncStatus();
if (!isOnline) {
return (
<StatusBar color="orange">
📴 Offline Data sparas lokalt
</StatusBar>
);
}
if (pendingCount > 0) {
return (
<StatusBar color="yellow">
Synkar {pendingCount} ändringar...
</StatusBar>
);
}
return (
<StatusBar color="green">
Synkad {formatTime(lastSync)}
</StatusBar>
);
};
```
### Per-item sync status
```jsx
const WorkoutLogItem = ({ log }) => {
return (
<View>
<Text>{log.exercise} {log.weight}kg × {log.reps}</Text>
{!log.synced && (
<Badge color="orange">Ej synkad</Badge>
)}
</View>
);
};
```
---
## Gravl Implementation Plan
### Phase 1: Local Storage
```
1. Implementera SQLite/IndexedDB
2. Spara ALL data lokalt först
3. UI visar alltid lokal data
4. Ingen sync ännu (100% offline)
```
### Phase 2: Basic Sync
```
1. Lägg till sync queue
2. POST nya workouts till server
3. Markera som synkade
4. Retry on failure
```
### Phase 3: Bi-directional Sync
```
1. Pull server-ändringar
2. Merge med lokal data
3. Conflict resolution
4. Multi-device support
```
### Phase 4: Real-time (optional)
```
1. WebSocket för live updates
2. Optimistic UI
3. Collaborative features
```
---
## Database Schema (Offline-optimerad)
```sql
-- Local SQLite schema
CREATE TABLE workouts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
local_id TEXT UNIQUE NOT NULL, -- UUID, genereras lokalt
server_id INTEGER, -- NULL tills synkad
-- Data
program_day_id INTEGER,
started_at TEXT,
completed_at TEXT,
notes TEXT,
-- Sync metadata
synced INTEGER DEFAULT 0,
sync_action TEXT DEFAULT 'create', -- 'create', 'update', 'delete'
local_updated_at TEXT,
server_updated_at TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE workout_sets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
local_id TEXT UNIQUE NOT NULL,
server_id INTEGER,
workout_local_id TEXT REFERENCES workouts(local_id),
exercise_id INTEGER,
set_number INTEGER,
weight REAL,
reps INTEGER,
rpe REAL,
synced INTEGER DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE sync_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
table_name TEXT,
local_id TEXT,
action TEXT, -- 'create', 'update', 'delete'
payload TEXT, -- JSON
attempts INTEGER DEFAULT 0,
last_attempt TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
-- Index för snabb sync-lookup
CREATE INDEX idx_workouts_synced ON workouts(synced);
CREATE INDEX idx_sync_queue_attempts ON sync_queue(attempts);
```
---
## Rekommendation för Gravl
### Tech Stack
```
Frontend: React (web) eller React Native (app)
Local DB: Dexie (IndexedDB wrapper) för web
expo-sqlite för native
Sync: Custom sync engine med retry logic
Backend: Befintlig Express/PostgreSQL
```
### Varför inte RxDB/CouchDB?
- Overhead för ett simpelt use case
- Gravl har enkel data (workouts, sets)
- Custom sync ger mer kontroll
### Nyckelprinciper
1. **Lokal data är sanning** — Servern är backup
2. **Aldrig blockera UI** — Sync sker i bakgrund
3. **Aldrig förlora data** — Queue allt
4. **Tydlig status** — Användaren vet vad som händer
---
## Källor
- Medium: Offline-First React Native (2026)
- OneUptime: React Native Data Sync
- dev.family: RxDB Architecture
- Google Developers: PWA Going Offline
- Monterail: PWA Dynamic Data
- SQLite.ai: SQLite Sync
- SQLite Cloud: OffSync
---
*Sammanställt 2026-02-15 av Bumblebee 🐝*
+386
View File
@@ -0,0 +1,386 @@
# Monetisering — Research för Gravl
## Marknadsöversikt
**Fitness app-marknaden:**
- 2025: ~$10 miljarder
- 2028 prognos: $15.6 miljarder
- Health & Fitness är top-kategorin för app revenue
**RevenueCat State of Subscription Apps 2025:**
- Health & Fitness: $0.63+ revenue per install efter 60 dagar
- Dubbelt median ($0.31 för alla kategorier)
- Låga årspriser = bättre retention (36%)
---
## Monetiseringsmodeller
### 1. Freemium (Mest vanlig)
**Så funkar det:**
- Gratis grundfunktioner
- Premium låser upp avancerade features
- Konverteringsmål: 2-5% free → paid
**Fördelar:**
- Låg tröskel för nya användare
- Stort användarbas
- Word-of-mouth
**Nackdelar:**
- Låg konverteringsrate
- Kostnad för gratis-användare
- Feature-balans är svår
**Fitness-exempel:**
- Hevy: Gratis loggning, premium för avancerade grafer
- Strong: 3 gratis routines, premium för obegränsat
### 2. Subscription (Prenumeration)
**Så funkar det:**
- Månads- eller årsbetalning
- Ofta med free trial
**Typiska priser (fitness):**
| App | Månads | Års | Trial |
|-----|--------|-----|-------|
| FITBOD | $12.99 | $79.99 | 3 workouts |
| Strong | $4.99 | $29.99 | 3 routines |
| Hevy | $2.99 | $23.99 | Generous free |
| Juggernaut AI | $35 | — | — |
**Trial konvertering (benchmark):**
- 25-60% trial → paid (bra apps)
- 7 dagar vs 30 dagar: Ingen signifikant skillnad
- "Pay upfront after trial" ökar konvertering
### 3. Paymium
**Så funkar det:**
- Betala för att ladda ner + in-app purchases
**2025 Insight:**
> "Paymium has emerged as the dominant monetization strategy for fitness apps targeting engaged, high-value audiences."
**Fördelar:**
- Filtrerar bort tire-kickers
- Högre ARPU
- Mer engagerade användare
**Nackdelar:**
- Mycket lägre downloads
- Kräver stark varumärke
- Svårare discovery
### 4. One-time Purchase
**Så funkar det:**
- En engångsbetalning, appen är din
**Reddit-sentiment:**
> "I'd happily pay $20 once for a good app. $100/year feels like a scam for a workout logger."
**Verklighet:**
- Svårt att underhålla utan löpande intäkt
- Fungerar för simpla appar
- Premium-tier kan vara one-time
### 5. Ads
**Fitness-användare HATAR ads:**
> "Ads in the middle of my workout? Instant uninstall."
**Om du måste:**
- Aldrig mitt i workout
- Endast i free-tier
- Banner, inte interstitial
---
## Pricing Psychology
### Principer som fungerar
#### 1. Anchoring (Förankring)
Visa det dyraste alternativet först:
```
┌────────────────────────────────────────┐
│ Premium Yearly $79.99/år │ ← Anchor
│ (Spara 50%!) = $6.67/mån │
├────────────────────────────────────────┤
│ Premium Monthly $12.99/mån │
├────────────────────────────────────────┤
│ Free $0 │
└────────────────────────────────────────┘
```
#### 2. Price Framing
```
❌ "$79.99 per år"
✅ "Mindre än en kaffe per vecka"
✅ "Billigare än ett PT-pass"
```
#### 3. Decoy Effect
Lägg till ett "dåligt" alternativ för att göra det önskade bättre:
```
Monthly: $12.99/mån
Quarterly: $32.99/kvartal (= $11/mån) ← Decoy
Yearly: $79.99/år (= $6.67/mån) ← Target
```
#### 4. Loss Aversion
```
"Du har tränat 47 pass i år. Uppgradera för att behålla din data!"
"Din streak på 23 dagar — fortsätt med Premium!"
```
#### 5. Social Proof
```
"Gå med 50,000+ användare som blivit starkare med Gravl"
"4.8 ★ på App Store"
```
---
## Free Trial Best Practices
### Trial Length
**Research:**
> "No significant difference between 7 and 30 day trials in conversion rate."
**Rekommendation:** 7 dagar är standard, 14 dagar för fitness (tid att se resultat)
### Trial Experience
1. **Full access** — Låt användare uppleva ALLT
2. **Onboarding** — Guida till value snabbt
3. **Reminders** — "3 dagar kvar av trial"
4. **Soft paywall** — "Trial slut, vill du fortsätta?"
### Conversion Tactics
```
Day 1: Welcome, visa premium features
Day 3: "Har du testat [killer feature]?"
Day 5: "Du har gjort X pass! Se din progress (premium)"
Day 6: "Sista dagen imorgon — 20% rabatt!"
Day 7: Soft paywall, erbjud förlängning
```
---
## Paywall Design
### Top Fitness Apps (UX Patterns)
#### 1. Value-first
```
┌────────────────────────────────────────┐
│ Bli starkare med Gravl │
│ │
│ ✓ AI-anpassade program │
│ ✓ Unlimited routines │
│ ✓ Progress analytics │
│ ✓ Offline mode │
│ │
│ ┌──────────────────────────────────┐ │
│ │ Årsplan 399 kr/år │ │
│ │ Spara 50% (33 kr/mån) │ │
│ └──────────────────────────────────┘ │
│ │
│ [Månadsplan 69 kr/mån] │
│ │
│ [Fortsätt gratis med begränsningar] │
└────────────────────────────────────────┘
```
#### 2. Trial-fokuserad
```
┌────────────────────────────────────────┐
│ Testa Premium gratis i 7 dagar │
│ │
│ Du kan avbryta när som helst. │
│ Ingen betalning förrän trial slutar. │
│ │
│ [Starta gratis trial] │
│ │
│ Efter trial: 399 kr/år │
│ │
│ [Nej tack, fortsätt gratis] │
└────────────────────────────────────────┘
```
#### 3. Social proof
```
┌────────────────────────────────────────┐
│ "Gravl ändrade hur jag tränar" │
│ ★★★★★ — Marcus, Stockholm │
│ │
│ "Äntligen en app utan bloat" │
│ ★★★★★ — Emma, Göteborg │
│ │
│ 50,000+ nöjda användare │
│ │
│ [Gå med nu — 399 kr/år] │
└────────────────────────────────────────┘
```
---
## Pricing för Gravl
### Rekommenderad modell: Freemium + Subscription
#### Free Tier
**Inkluderar:**
- Obegränsade custom routines
- Basic workout logging
- Rest timer
- Mörkt tema
- Offline-stöd
**Begränsningar:**
- Ingen AI-coach
- Basic progress grafer (senaste 30 dagar)
- Ingen exercise substitution
- Ingen export
#### Premium Tier
**Inkluderar allt i Free, plus:**
- AI-coach (conversational)
- Avancerade progress analytics
- Exercise substitution
- Dagsform-anpassning
- Data export
- Priority support
### Prissättning (Sverige)
| Plan | Pris | Pris/mån | vs konkurrenter |
|------|------|----------|-----------------|
| **Månads** | 69 kr | 69 kr | Under FITBOD, över Hevy |
| **Års** | 399 kr | 33 kr | Konkurrenskraftigt |
| **Lifetime** | 999 kr | — | För early adopters |
### Positionering
```
Billigare ←───────────────────→ Dyrare
┌─────┐
│Gravl│ (value sweet spot)
└─────┘
┌────┐ ┌──────┐ ┌──────────┐
│Hevy│ │Strong│ │ FITBOD │
└────┘ └──────┘ └──────────┘
Gratis $30/år $79+/år
```
---
## Conversion Funnel
### Metrics att tracka
| Metric | Benchmark | Target |
|--------|-----------|--------|
| Free → Trial | 10-20% | 15% |
| Trial → Paid | 25-60% | 40% |
| Month 1 retention | 80-90% | 85% |
| Year 1 retention | 50-70% | 60% |
| ARPU | $0.63 (60d) | $0.70+ |
### Paywall Placement
| Trigger | Konvertering | Risk |
|---------|--------------|------|
| **Onboarding** | Hög | Kan skrämma |
| **After first workout** | Medel-Hög | Bra timing |
| **Feature-locked** | Medel | Frustrerande |
| **After value shown** | Högst | Kräver patience |
**Rekommendation:** Soft paywall efter första passet + feature-lock för AI.
---
## Lokala Betalningsmetoder (Sverige)
### Rekommenderade
- **Swish** — Populärt, men komplext för subscription
- **Klarna** — "Betala senare", bra för årsplaner
- **Apple Pay / Google Pay** — Standard
- **Kort** — Via Stripe
### Implementation
```
iOS: StoreKit 2 (App Store billing)
Android: Google Play Billing
Web: Stripe (med Klarna/Swish add-ons)
```
---
## Revenue Projections
### Scenario: 10,000 MAU
| Metric | Value |
|--------|-------|
| Free users | 8,500 (85%) |
| Trial starters | 1,500 (15%) |
| Paid conversions | 600 (40% of trial) |
| Avg revenue/paid user | 399 kr/år |
| **Annual Revenue** | **239,400 kr** |
### Growth Path
```
Year 1: 600 paying users × 399 kr = 239,400 kr
Year 2: 2,000 paying × 399 kr = 798,000 kr
Year 3: 5,000 paying × 399 kr = 1,995,000 kr
```
---
## Anti-patterns att undvika
| Gör inte | Varför |
|----------|--------|
| ❌ Ads i workout | Instant uninstall |
| ❌ Paywall på basic logging | Konkurrenter är gratis |
| ❌ Dark patterns | Förstör förtroende |
| ❌ Fake scarcity | Genomskådas |
| ❌ Subscription för allt | "Subscription fatigue" |
---
## Källor
- RevenueCat State of Subscription Apps 2025
- AppWill: Paymium for Fitness Apps
- Business of Apps: Monetization Strategies
- Tesseract Academy: Fitness App Monetization 2026
- Apphud: Trial Conversion Rates
- Phoenix Strategy Group: Freemium vs Subscription
- Crazy Egg: Free-to-Paid Conversion
---
*Sammanställt 2026-02-15 av Bumblebee 🐝*