SleepSync - FullStack App made with Xano and Bubble

Published: (December 14, 2025 at 03:43 PM EST)
5 min read
Source: Dev.to

Source: Dev.to

Overview

This submission is for the Xano AI‑Powered Backend Challenge: Full‑Stack, AI‑First Application.
SleepSync is a full‑stack app that unifies sleep and health data from Oura and Whoop wearables into a Truth Score™ – a unified metric (e.g., 0‑100) calculated by averaging or weighting key metrics such as sleep duration, readiness/recovery scores, HRV, resting heart rate, etc. For the initial version, API integrations are simulated by allowing users to upload CSV files.

AI Prompt Used

Generate a complete Xano backend in XanoScript for a sleep data unification app called SleepSync. Include: Authentication (email/password signup/login endpoints returning JWT). Database tables: users (id, email, password_hash, created_at), oura_data (user_id, date, total_sleep_seconds, readiness_score, resting_heart_rate, hrv, etc. – base on Oura CSV fields like deep_sleep_duration, respiratory_rate), whoop_sleeps (user_id, cycle_start_time, asleep_duration_min, recovery_score, resting_heart_rate_bpm, hrv_ms, etc.), unified_data (user_id, date, unified_sleep_hours, unified_rhr, unified_hrv, truth_score). API Endpoints: POST /auth/signup (create user, hash password, generate user_id), POST /auth/login (validate, return JWT), POST /upload/csv (authenticated, accept file/type 'oura' or 'whoop', parse CSV, insert with user_id from JWT, normalize timestamps to UTC), GET /data/unified (authenticated, query by user_id/date range, compute unified metrics like unified_sleep_hours = avg(Oura total_sleep/3600, Whoop asleep_min/60); truth_score = avg(Oura readiness, Whoop recovery); store in unified_data). Use XanoScript syntax ($db, $auth). Handle errors, nulls (default 0). Assume CSVs have headers. Make scalable for multiple uploads.

Xano Backend Script (Excerpt)

workspace SleepSyncBackend {
  acceptance = {ai_terms: false}
  preferences = {
    internal_docs    : false
    track_performance: true
    sql_names        : false
    sql_columns      : true
  }
}

// Users table
$db.createTable('users', [
  {name: 'id', type: 'uuid', primary: true},
  {name: 'email', type: 'string', unique: true},
  {name: 'password_hash', type: 'string'},
  {name: 'created_at', type: 'timestamp', default: 'now()'}
]);

// Oura data table
$db.createTable('oura_data', [
  {name: 'id', type: 'uuid', primary: true},
  {name: 'user_id', type: 'uuid', foreignKey: 'users.id'},
  {name: 'date', type: 'date'},
  {name: 'total_sleep_seconds', type: 'int'},
  {name: 'readiness_score', type: 'float'},
  {name: 'resting_heart_rate', type: 'int'},
  {name: 'hrv', type: 'float'}
  // add other Oura fields as needed
]);

// Whoop sleeps table
$db.createTable('whoop_sleeps', [
  {name: 'id', type: 'uuid', primary: true},
  {name: 'user_id', type: 'uuid', foreignKey: 'users.id'},
  {name: 'cycle_start_time', type: 'timestamp'},
  {name: 'asleep_duration_min', type: 'int'},
  {name: 'recovery_score', type: 'float'},
  {name: 'resting_heart_rate_bpm', type: 'int'},
  {name: 'hrv_ms', type: 'float'}
  // add other Whoop fields as needed
]);

// Unified data table
$db.createTable('unified_data', [
  {name: 'id', type: 'uuid', primary: true},
  {name: 'user_id', type: 'uuid', foreignKey: 'users.id'},
  {name: 'date', type: 'date'},
  {name: 'unified_sleep_hours', type: 'float'},
  {name: 'unified_rhr', type: 'float'},
  {name: 'unified_hrv', type: 'float'},
  {name: 'truth_score', type: 'float'}
]);

// Authentication endpoints
$api.post('/auth/signup', async (req, res) => {
  const {email, password} = req.body;
  const hash = await $auth.hashPassword(password);
  const user = await $db.insert('users', {email, password_hash: hash});
  const token = $auth.createJwt({user_id: user.id});
  res.json({token});
});

$api.post('/auth/login', async (req, res) => {
  const {email, password} = req.body;
  const user = await $db.findOne('users', {email});
  if (!user || !(await $auth.verifyPassword(password, user.password_hash))) {
    return res.status(401).json({error: 'Invalid credentials'});
  }
  const token = $auth.createJwt({user_id: user.id});
  res.json({token});
});

// CSV upload endpoint (simplified for Oura only)
$api.post('/upload/csv', $auth.requireJwt, async (req, res) => {
  const {type} = req.body; // 'oura' or 'whoop'
  const file = req.files.csv; // assume multipart/form-data
  const rows = await $utils.parseCsv(file.buffer);
  const userId = req.user.user_id;

  if (type === 'oura') {
    for (const row of rows) {
      await $db.insert('oura_data', {
        user_id: userId,
        date: $utils.toDate(row['date']),
        total_sleep_seconds: parseInt(row['total_sleep_seconds'] ?? 0),
        readiness_score: parseFloat(row['readiness_score'] ?? 0),
        resting_heart_rate: parseInt(row['resting_heart_rate'] ?? 0),
        hrv: parseFloat(row['hrv'] ?? 0)
        // map additional fields as needed
      });
    }
  } else if (type === 'whoop') {
    // similar logic for whoop_sleeps
  } else {
    return res.status(400).json({error: 'Invalid type'});
  }

  res.json({message: 'CSV processed successfully'});
});

// Unified data endpoint
$api.get('/data/unified', $auth.requireJwt, async (req, res) => {
  const {start_date, end_date} = req.query;
  const userId = req.user.user_id;

  const ouraRows = await $db.select('oura_data', {
    where: {user_id: userId, date: {$gte: start_date, $lte: end_date}}
  });
  const whoopRows = await $db.select('whoop_sleeps', {
    where: {user_id: userId, cycle_start_time: {$gte: start_date, $lte: end_date}}
  });

  // Compute unified metrics per day (simplified aggregation)
  const unified = $utils.aggregateByDate(ouraRows, whoopRows, (oura, whoop) => ({
    unified_sleep_hours: (
      (oura.total_sleep_seconds / 3600) +
      (whoop.asleep_duration_min / 60)
    ) / 2,
    truth_score: (oura.readiness_score + whoop.recovery_score) / 2,
    unified_rhr: (oura.resting_heart_rate + whoop.resting_heart_rate_bpm) / 2,
    unified_hrv: (oura.hrv + whoop.hrv_ms) / 2
  }));

  // Store results
  for (const day of unified) {
    await $db.upsert('unified_data', {
      user_id: userId,
      date: day.date,
      unified_sleep_hours: day.unified_sleep_hours,
      truth_score: day.truth_score,
      unified_rhr: day.unified_rhr,
      unified_hrv: day.unified_hrv
    });
  }

  res.json(unified);
});

Configuration Steps

1. Set Up Xano

  1. Create a new workspace (e.g., SleepSyncBackend).
  2. Add the tables shown above (users, oura_data, whoop_sleeps, unified_data).
  3. Paste the XanoScript code into the API section, creating the corresponding endpoints.
  4. Enable JWT authentication on the protected routes (/upload/csv, /data/unified).

2. Connect Bubble

  1. Follow the official guide:

  2. In Bubble, install the API Connector plugin.

  3. Add the Xano base URL and configure the following calls:

    • Signup – POST /auth/signup (email, password) → store returned JWT.
    • Login – POST /auth/login (email, password) → store JWT.
    • Upload CSV – POST /upload/csv (multipart/form‑data, include type and file) → include Authorization: Bearer header.
    • Get Unified Data – GET /data/unified?start_date=...&end_date=... → include JWT header.
  4. Use Bubble’s workflow actions to:

    • Prompt users to upload Oura or Whoop CSV files.
    • Trigger the upload API call.
    • Display the returned unified metrics (sleep hours, truth score, etc.) in repeating groups or charts.

3. Testing

  • Create a test user via the signup endpoint.
  • Upload sample Oura CSV files (ensure headers match the field names used in the script).
  • Verify that rows appear in the oura_data table.
  • Call /data/unified and confirm that the computed truth scores are stored in unified_data.

Resources

  • Xano Documentation
  • Bubble API Connector
  • Official Xano‑Bubble Integration Guide

Current Challenges

  • Community support is limited; there is no official Discord or chat channel, making troubleshooting rely heavily on documentation or AI assistance.

End of submission.

Back to Blog

Related posts

Read more »