SleepSync - FullStack App made with Xano and Bubble
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
- Create a new workspace (e.g.,
SleepSyncBackend). - Add the tables shown above (
users,oura_data,whoop_sleeps,unified_data). - Paste the XanoScript code into the API section, creating the corresponding endpoints.
- Enable JWT authentication on the protected routes (
/upload/csv,/data/unified).
2. Connect Bubble
-
Follow the official guide:
-
In Bubble, install the API Connector plugin.
-
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, includetypeand file) → includeAuthorization: Bearerheader. - Get Unified Data – GET
/data/unified?start_date=...&end_date=...→ include JWT header.
- Signup – POST
-
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_datatable. - Call
/data/unifiedand confirm that the computed truth scores are stored inunified_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.