Developer Scripts

GilRobo Cloud Sync — Setup Guide

How It Works

GilRobo stores all data in your browser's localStorage. The Cloud Sync engine adds a thin HTTP layer on top — it can push local data to your cloud database, and pull remote data back in, merging by record ID so nothing is overwritten unexpectedly.

🔁 Two-Way Sync Model
Push: Sends all local records to the cloud (upsert by ID).
Pull: Downloads cloud records and merges them with local data — remote wins on conflict.
Two-Way: Pull first, then Push — guarantees both sides are identical afterward.

What data is synced?

Table / KeyContentsGroup
gs_studentsClassroom student roster, scores, quiz historyClassroom
gs_quiz_resultsAll classroom quiz results with scores per studentClassroom
gs_lecturesClassroom sessions — transcript, duration, attendanceClassroom
gs_settingsTeacher config — name, subject, schedule, blocked topicsClassroom
qm_quiz_historyHistory Quiz tab results with full question/answer dataQuiz
qm_quiz2_historyIELTS / Quiz 2 resultsQuiz
qm_tutorial_historyTutorial session historyQuiz
qm_custom_topicsAdmin-created custom topics (title + body)Quiz
qm_custom_questionsAdmin-created custom question banksQuiz
gr_pd_historyParent Day consultation transcriptsParent Day
pd_admin_settingsParent Day blocked topics & admin rulesParent Day

Admin Workflow — Loading New Data

Admin adds data to the cloud database
New quiz questions, custom topics, student records, or lesson plans inserted directly into the DB via your preferred admin tool (Azure Portal, WordPress admin, AWS Console, or your custom CMS).
App pulls from cloud
Teacher/admin clicks ☁️ → Pull All (or auto-sync fires). New records are merged into localStorage — existing records keep their local changes, new records appear immediately.
Data appears in Parent Day, Lesson Planner, Quiz tab
New custom topics show in the subject dropdown. New students appear in the roster. New quiz content is available. The admin's data is now live in the app.
App activity pushes back
Quiz results, consultations, and session transcripts push back to the cloud on the next sync cycle — available for admin reporting, analytics, or export.
⚠️ Browser Storage Limits
localStorage is limited to ~5–10 MB per domain. For large schools with many students and consultation transcripts, ensure your auto-sync is frequent enough to clear older records from localStorage after archiving them in the cloud.

🏗System Architecture

GilRobo App
localStorage
Cloud Sync Engine
fetch() / REST
WordPress REST
/wp-json/gilrobo/v1/
or
Azure Functions
/api/gilrobo/
or
AWS API Gateway
/prod/gilrobo/
or
Custom REST API
any backend
MySQL / PostgreSQL
WordPress DB
or
Cosmos DB / Azure SQL
Azure
or
DynamoDB / RDS
AWS
or
Any SQL / NoSQL DB
Custom

API Contract — all providers use the same 4 endpoints

MethodPathDescription
GET/gilrobo/pingHealth check — returns {"status":"ok"}
POST/gilrobo/tablesCreate table — body: {"table":"gs_students"}
PUT/gilrobo/sync/{table}Push — body: {"data":[…],"pushedAt":"ISO"} → returns {"count":N}
GET/gilrobo/sync/{table}Pull — returns {"data":[…]}

🗄Database Schema

All tables use a simple structure: each row is one JSON record, keyed by its id field. This makes the sync logic trivial — upsert by ID on push, merge by ID on pull.

SQL Schema (MySQL / PostgreSQL / Azure SQL / RDS)

-- Run this once to create all GilRobo sync tables
-- Works on MySQL 5.7+, PostgreSQL 12+, Azure SQL, Amazon RDS

CREATE TABLE IF NOT EXISTS gilrobo_sync (
    table_name   VARCHAR(80)  NOT NULL,
    record_id    VARCHAR(120) NOT NULL,
    data         JSON         NOT NULL,
    pushed_at    DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    pulled_at    DATETIME     NULL,
    PRIMARY KEY (table_name, record_id)
);

-- Optional: per-table views for readability
CREATE OR REPLACE VIEW vw_gs_students AS
  SELECT record_id AS id, data, pushed_at FROM gilrobo_sync WHERE table_name='gs_students';

CREATE OR REPLACE VIEW vw_qm_quiz_history AS
  SELECT record_id AS id, data, pushed_at FROM gilrobo_sync WHERE table_name='qm_quiz_history';

-- Index for fast table scans
CREATE INDEX idx_gilrobo_table ON gilrobo_sync(table_name, pushed_at DESC);
SQL

DynamoDB Schema (AWS)

{
  "TableName": "gilrobo_sync",
  "BillingMode": "PAY_PER_REQUEST",
  "KeySchema": [
    { "AttributeName": "pk",  "KeyType": "HASH"  },
    { "AttributeName": "sk",  "KeyType": "RANGE" }
  ],
  "AttributeDefinitions": [
    { "AttributeName": "pk", "AttributeType": "S" },
    { "AttributeName": "sk", "AttributeType": "S" }
  ]
}
-- pk = table_name (e.g. "gs_students")
-- sk = record_id  (e.g. "s_1234567890")
-- Additional attributes: data (Map), pushed_at (String ISO date)
JSON

Cosmos DB Schema (Azure)

// Container: gilrobo_sync
// Partition key: /table_name
// Each document:
{
  "id": "{table_name}#{record_id}",   // Cosmos document ID
  "table_name": "gs_students",
  "record_id": "s_1234567890",
  "data": { ... },                    // Full GilRobo record
  "pushed_at": "2025-03-13T10:00:00Z"
}
JSON

Key Data Structures

gs_students
id string
name string
age number|null
cls string
status approved|pending
score number
quizHistory array
registeredAt ISO date
qm_quiz_history
id string
date ISO date
subject string
difficulty string
players array of scores
questions array
isPublic boolean
gr_pd_history
id string
student string
parents string
mode private|family…
lang string
date ISO date
transcript array
qm_custom_topics
id string
title string
body string (content)

🔵WordPress Setup

✅ Easiest Option
If you already have a WordPress school site, this takes about 10 minutes. The plugin creates the DB table and registers the REST routes automatically.

Step 1 — Install the GilRobo Sync Plugin

Create a file gilrobo-sync/gilrobo-sync.php in your WordPress wp-content/plugins/ folder with this code, then activate it in WP Admin → Plugins.

<?php
/**
 * Plugin Name: GilRobo Sync
 * Description: Bidirectional sync endpoint for GilRobo AI Learning Family app.
 * Version: 1.0
 * Author: GilRobo
 */
if(!defined('ABSPATH')) exit;

register_activation_hook(__FILE__, 'gilrobo_create_table');
function gilrobo_create_table(){
    global $wpdb;
    $table = $wpdb->prefix.'gilrobo_sync';
    $charset = $wpdb->get_charset_collate();
    $sql = "CREATE TABLE IF NOT EXISTS $table (
        table_name  VARCHAR(80)  NOT NULL,
        record_id   VARCHAR(120) NOT NULL,
        data        LONGTEXT     NOT NULL,
        pushed_at   DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (table_name, record_id),
        INDEX idx_table (table_name, pushed_at DESC)
    ) $charset;";
    require_once ABSPATH.'wp-admin/includes/upgrade.php';
    dbDelta($sql);
}

add_action('rest_api_init', 'gilrobo_register_routes');
function gilrobo_register_routes(){
    $ns = 'gilrobo/v1';
    // Health check
    register_rest_route($ns, '/ping', ['methods'=>'GET','callback'=>fn()=>['status'=>'ok','time'=>current_time('c')],'permission_callback'=>'gilrobo_auth']);
    // Create table (no-op — table is created on activation, kept for API parity)
    register_rest_route($ns, '/tables', ['methods'=>'POST','callback'=>fn($r)=>['created'=>true,'table'=>sanitize_text_field($r['table'])],'permission_callback'=>'gilrobo_auth']);
    // Push (upsert)
    register_rest_route($ns, '/sync/(?P<table>[a-z0-9_]+)', ['methods'=>'PUT','callback'=>'gilrobo_push','permission_callback'=>'gilrobo_auth']);
    // Pull
    register_rest_route($ns, '/sync/(?P<table>[a-z0-9_]+)', ['methods'=>'GET','callback'=>'gilrobo_pull','permission_callback'=>'gilrobo_auth']);
}

function gilrobo_auth(WP_REST_Request $request){
    // Accepts: Application Password (Basic Auth) or custom header X-GilRobo-Key
    $custom_key = get_option('gilrobo_api_key','');
    if($custom_key && $request->get_header('X-GilRobo-Key')===$custom_key) return true;
    return current_user_can('manage_options') || is_user_logged_in();
}

function gilrobo_push(WP_REST_Request $request){
    global $wpdb;
    $table_name = $wpdb->prefix.'gilrobo_sync';
    $gr_table   = sanitize_text_field($request['table']);
    $body       = $request->get_json_params();
    $records    = $body['data'] ?? [];
    if(!is_array($records)) return new WP_Error('invalid','data must be array',['status'=>400]);
    $count = 0;
    foreach($records as $rec){
        $id = sanitize_text_field($rec['id'] ?? uniqid('r_'));
        $wpdb->replace($table_name,[
            'table_name' => $gr_table,
            'record_id'  => $id,
            'data'       => wp_json_encode($rec),
            'pushed_at'  => current_time('mysql'),
        ]);
        $count++;
    }
    return ['ok'=>true,'count'=>$count,'table'=>$gr_table];
}

function gilrobo_pull(WP_REST_Request $request){
    global $wpdb;
    $table_name = $wpdb->prefix.'gilrobo_sync';
    $gr_table   = sanitize_text_field($request['table']);
    $rows = $wpdb->get_results(
        $wpdb->prepare("SELECT data FROM $table_name WHERE table_name=%s ORDER BY pushed_at DESC",$gr_table)
    );
    $data = array_map(fn($r)=>json_decode($r->data,true),$rows);
    return ['ok'=>true,'data'=>$data,'count'=>count($data),'table'=>$gr_table];
}

// Admin settings page to set custom API key
add_action('admin_menu','gilrobo_admin_menu');
function gilrobo_admin_menu(){add_options_page('GilRobo Sync','GilRobo Sync','manage_options','gilrobo-sync','gilrobo_settings_page');}
function gilrobo_settings_page(){
    if(isset($_POST['gilrobo_key'])){update_option('gilrobo_api_key',sanitize_text_field($_POST['gilrobo_key']));echo'<div class="updated"><p>Saved!</p></div>';}
    $key=get_option('gilrobo_api_key','');
    echo '<div class="wrap"><h1>GilRobo Sync Settings</h1>
    <form method="post">
    <table class="form-table">
    <tr><th>Custom API Key (X-GilRobo-Key)</th>
    <td><input name="gilrobo_key" value="'.esc_attr($key).'" style="width:400px"/></td></tr>
    <tr><th>REST Base URL</th>
    <td><code>'.get_rest_url(null,"gilrobo/v1").'</code> — use this in the GilRobo app.</td></tr>
    </table>
    <p class="submit"><button class="button-primary">Save</button></p>
    </form></div>';
}
PHP

Step 2 — Get your REST URL

After activating, go to WP Admin → Settings → GilRobo Sync. Copy the REST Base URL shown there. It looks like:

https://yourschool.com/wp-json/gilrobo/v1

Step 3 — Configure the app

  1. Click ☁️ (floating button, bottom-right of GilRobo app)
  2. Select provider: WordPress REST API
  3. Paste the REST URL into WordPress Site URL
  4. In WP Admin → Users → your user → Application Passwords, create a password and paste it (format: base64(username:app-password))
  5. Click 🔌 Test Connection — should say "Connected ✅"
  6. Click 🗄️ Create Tables, then 🔁 Two-Way Sync
⚠️ CORS
If the app is served from a different domain than WordPress, add this to your theme's functions.php:
add_action('rest_api_init',function(){
  header('Access-Control-Allow-Origin: *');
  header('Access-Control-Allow-Headers: Authorization, Content-Type, X-GilRobo-Key, X-WP-Nonce');
});

☁️Microsoft Azure Setup

Architecture
Azure Functions (HTTP trigger) → Azure SQL Database or Cosmos DB. Deploy with the ARM template below.

Option A — Azure SQL + Azure Functions (Node.js)

// Azure Function: gilrobo-sync/index.js
// Set these Application Settings in Azure Portal:
//   SQL_CONN  = your Azure SQL connection string
//   GILROBO_KEY = your chosen API key

const sql = require('mssql');
const crypto = require('crypto');

module.exports = async function(context, req){
  // Auth
  const key = req.headers['x-api-key'] || req.headers['x-gilrobo-key'] || '';
  if(key !== process.env.GILROBO_KEY)
    return context.res = {status:401, body:{error:'Unauthorized'}};

  const path = req.params.route || '';    // e.g. "sync/gs_students"
  const [,action,table] = path.match(/^(ping|tables|sync\/([a-z0-9_]+))$/) || [];

  await sql.connect(process.env.SQL_CONN);

  if(path === 'ping'){
    return context.res = {body:{status:'ok',time:new Date().toISOString()}};
  }

  if(path === 'tables'){
    await sql.query\`
      IF NOT EXISTS(SELECT * FROM sys.tables WHERE name='gilrobo_sync')
      CREATE TABLE gilrobo_sync(
        table_name  NVARCHAR(80)  NOT NULL,
        record_id   NVARCHAR(120) NOT NULL,
        data        NVARCHAR(MAX) NOT NULL,
        pushed_at   DATETIME2     NOT NULL DEFAULT GETUTCDATE(),
        CONSTRAINT pk_gr PRIMARY KEY(table_name,record_id)
      )\`;
    return context.res = {body:{created:true}};
  }

  if(req.method === 'PUT' && table){  // Push
    const records = (req.body && req.body.data) || [];
    let count = 0;
    for(const rec of records){
      const id = rec.id || crypto.randomUUID();
      await sql.query\`
        MERGE gilrobo_sync AS t
        USING (VALUES(${table},${id},${JSON.stringify(rec)},GETUTCDATE()))
          AS s(table_name,record_id,data,pushed_at)
        ON t.table_name=s.table_name AND t.record_id=s.record_id
        WHEN MATCHED THEN UPDATE SET t.data=s.data, t.pushed_at=s.pushed_at
        WHEN NOT MATCHED THEN INSERT(table_name,record_id,data,pushed_at) VALUES(s.table_name,s.record_id,s.data,s.pushed_at);\`;
      count++;
    }
    return context.res = {body:{ok:true,count,table}};
  }

  if(req.method === 'GET' && table){  // Pull
    const result = await sql.query\`
      SELECT data FROM gilrobo_sync WHERE table_name=${table} ORDER BY pushed_at DESC\`;
    const data = result.recordset.map(r => JSON.parse(r.data));
    return context.res = {body:{ok:true,data,count:data.length}};
  }

  context.res = {status:404,body:{error:'Not found'}};
};
Node.js

function.json (routing)

{
  "bindings": [
    {
      "authLevel": "function",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "route": "gilrobo/{*route}",
      "methods": ["GET","PUT","POST"]
    },
    { "type": "http", "direction": "out", "name": "res" }
  ]
}
JSON

Configure in GilRobo app

  1. Deploy the function app to Azure. Copy the Function URL (e.g. https://myapp.azurewebsites.net/api)
  2. In GilRobo → ☁️ → Provider: Microsoft Azure
  3. Paste the function base URL (up to /api) into Azure API Endpoint
  4. Paste your Function Key (from Azure Portal → Function → Keys) into Azure Function Key
  5. Test, Create Tables, Sync

🟠Amazon AWS Setup

Architecture
API Gateway (REST API) → Lambda (Node.js) → DynamoDB. Fully serverless, pay-per-request.

Lambda Function (Node.js 20)

// lambda/index.mjs — deploy as a Lambda function
// Environment variables:
//   GILROBO_KEY  = your chosen API key
//   AWS_REGION   = your region (set automatically)

import {DynamoDBClient,PutItemCommand,ScanCommand,CreateTableCommand} from '@aws-sdk/client-dynamodb';
import {marshall,unmarshall} from '@aws-sdk/util-dynamodb';

const ddb = new DynamoDBClient({});
const TABLE = 'gilrobo_sync';

export const handler = async(event) => {
  const key = event.headers?.['x-api-key'] || event.headers?.['X-API-Key'] || '';
  if(key !== process.env.GILROBO_KEY)
    return {statusCode:401,body:JSON.stringify({error:'Unauthorized'})};

  const method = event.httpMethod;
  const path   = (event.pathParameters?.proxy || '').toLowerCase();
  const cors   = {'Access-Control-Allow-Origin':'*','Content-Type':'application/json'};

  if(path === 'ping')
    return {statusCode:200,headers:cors,body:JSON.stringify({status:'ok',time:new Date().toISOString()})};

  if(path === 'tables' && method==='POST'){
    try{
      await ddb.send(new CreateTableCommand({
        TableName: TABLE,
        BillingMode: 'PAY_PER_REQUEST',
        KeySchema: [{AttributeName:'pk',KeyType:'HASH'},{AttributeName:'sk',KeyType:'RANGE'}],
        AttributeDefinitions: [{AttributeName:'pk',AttributeType:'S'},{AttributeName:'sk',AttributeType:'S'}],
      }));
    }catch(e){ if(e.name!=='ResourceInUseException') throw e; }
    return {statusCode:200,headers:cors,body:JSON.stringify({created:true})};
  }

  const match = path.match(/^sync\/([a-z0-9_]+)$/);
  if(!match) return {statusCode:404,headers:cors,body:JSON.stringify({error:'Not found'})};
  const grTable = match[1];

  if(method==='PUT'){  // Push
    const {data=[]} = JSON.parse(event.body||'{}');
    let count=0;
    for(const rec of data){
      const id = rec.id||`r_${Date.now()}_${count}`;
      await ddb.send(new PutItemCommand({
        TableName: TABLE,
        Item: marshall({pk:grTable,sk:id,data:rec,pushed_at:new Date().toISOString()},{removeUndefinedValues:true})
      }));
      count++;
    }
    return {statusCode:200,headers:cors,body:JSON.stringify({ok:true,count,table:grTable})};
  }

  if(method==='GET'){  // Pull
    const result = await ddb.send(new ScanCommand({
      TableName: TABLE,
      FilterExpression: 'pk = :t',
      ExpressionAttributeValues: marshall({':t':grTable})
    }));
    const data = (result.Items||[]).map(i=>unmarshall(i).data).filter(Boolean);
    return {statusCode:200,headers:cors,body:JSON.stringify({ok:true,data,count:data.length})};
  }

  return {statusCode:405,headers:cors,body:JSON.stringify({error:'Method not allowed'})};
};
Node.js

API Gateway setup

  1. Create a REST API in API Gateway
  2. Create resource /gilrobo/{proxy+} with Lambda proxy integration
  3. Enable API Key Required on the method and create an API key + usage plan
  4. Deploy to stage prod
  5. Copy the Invoke URL (e.g. https://xxxx.execute-api.eu-west-2.amazonaws.com/prod)

Configure in GilRobo app

  1. Provider: Amazon AWS
  2. Paste invoke URL into API Gateway Endpoint
  3. Paste API Gateway key into API Gateway Key
  4. Test Connection → Create Tables → Sync

💜Microsoft Dataverse Setup

What is Dataverse?
Microsoft Dataverse is the cloud database behind Power Platform (Power Apps, Power Automate, Power BI, Dynamics 365). It stores data as typed tables with row-level security, relationships, business rules, and native integration with all Microsoft 365 services. GilRobo connects to it directly via the Dataverse Web API (OData v4) using OAuth2 client credentials.

Architecture

GilRobo App
OAuth2 Token
login.microsoftonline.com
Dataverse Web API
OData v4 / REST
Dataverse Tables
cr_gilrobo_*

Step 1 — Register an Azure App

Go to Azure Portal → Azure Active Directory → App registrations → New registration
Name it "GilRobo Sync". Under Supported account types, choose "Accounts in this organizational directory only".
Add API Permission: Dataverse
In the app → API permissions → Add a permission → APIs my organization uses → search "Dataverse" → select "user_impersonation" → Grant admin consent.
Create a Client Secret
App → Certificates & secrets → New client secret → copy the Value immediately (it's only shown once). Also copy the Application (client) ID and Directory (tenant) ID from the Overview page.
Add App as Dataverse Application User
Go to Power Platform Admin Center → your Environment → Settings → Users + permissions → Application users → New app user → select your app registration → assign the "System Administrator" role (or a custom role with read/write on your custom tables).

Step 2 — Create custom tables in Power Apps

Dataverse tables for GilRobo follow the naming convention cr_gilrobo_{tablename} where {tablename} replaces underscores with the letter x:

GilRobo KeyDataverse Table NamePurpose
gs_studentscr_gilrobogs_studentsStudent roster
qm_quiz_historycr_gilroboqmxquizxhistoryQuiz results
gr_pd_historycr_gilrobogr_pdxhistoryParent Day transcripts
gs_lecturescr_gilrobogs_lecturesClassroom sessions

For each table, create these columns in Power Apps → Tables → New table:

Column NameTypeNotes
cr_recordidSingle line of textAlternate key — must be unique
cr_tablenameSingle line of texte.g. "gs_students"
cr_dataMultiple lines of textFull JSON record
cr_pushedatDate and timeLast push timestamp
⚠️ Alternate Key
For upsert to work, you must mark cr_recordid as an Alternate Key in Power Apps → Table → Keys → Add key. This allows PATCH requests to match by record ID rather than the internal Dataverse row GUID.

Step 3 — Configure in GilRobo app

Provider:         💜 Microsoft Dataverse
Environment URL:  https://yourorg.crm11.dynamics.com
                  (find in Power Platform Admin Center → Environments → your env → Details)
Tenant ID:        from Azure Portal → Azure AD → Overview
App (Client) ID:  from Azure Portal → App registration → Overview
Client Secret:    value you copied when creating the secret
Config

Click 🔌 Test Connection — it will request an OAuth2 token and call the Dataverse metadata endpoint. A successful response means the app registration is correctly configured.

Step 4 — Create tables, then sync

  1. Click 🗄️ Create Tables — this verifies each table entity is accessible
  2. Click 🔁 Two-Way Sync to do an initial full sync
  3. Data now lives in your Dataverse environment and can be viewed/edited in Power Apps, built into Power BI dashboards, triggered in Power Automate flows, or used in Dynamics 365

Power BI Integration

Once data is in Dataverse, connect Power BI Desktop to it: Get Data → Dataverse → select your environment → load the cr_gilrobo_* tables. Build dashboards showing quiz trends, student performance, parent consultation history, and more — all refreshed automatically on each GilRobo sync.

Power Automate Integration

Create flows triggered by Dataverse row changes: e.g. "When a new row is added to cr_gilrobogs_students → send a welcome email via Outlook". Or "When a student's quiz average drops below 60% → notify the teacher in Teams".

✅ CORS note
Dataverse Web API supports CORS from browser JavaScript by default. You do not need a proxy. The OAuth2 token request goes to login.microsoftonline.com which also has CORS headers set. The entire flow works entirely from the browser.

🌐Custom REST API Setup

Any backend that exposes the 4 GilRobo endpoints works. Here's a complete reference implementation in Node.js/Express and Python/Flask.

Node.js / Express

// server.js — npm install express cors better-sqlite3
const express = require('express');
const cors    = require('cors');
const Database= require('better-sqlite3');
const app     = express();
const db      = new Database('gilrobo.db');
const API_KEY = process.env.GILROBO_KEY || 'change-me';

db.exec(`CREATE TABLE IF NOT EXISTS gilrobo_sync(
  table_name TEXT NOT NULL, record_id TEXT NOT NULL,
  data TEXT NOT NULL, pushed_at TEXT NOT NULL,
  PRIMARY KEY(table_name,record_id)
)`);

app.use(cors());
app.use(express.json({limit:'10mb'}));

// Auth middleware
app.use('/gilrobo', (req,res,next) => {
  const k = req.headers['x-api-key'] || req.headers['authorization']?.replace('Bearer ','');
  if(k !== API_KEY) return res.status(401).json({error:'Unauthorized'});
  next();
});

app.get('/gilrobo/ping', (_,res) => res.json({status:'ok',time:new Date().toISOString()}));

app.post('/gilrobo/tables', (req,res) => {
  // Table already created at startup; this is a no-op for SQL backends
  res.json({created:true, table:req.body.table});
});

app.put('/gilrobo/sync/:table', (req,res) => {
  const {table} = req.params;
  const records = req.body?.data || [];
  const stmt = db.prepare(`INSERT OR REPLACE INTO gilrobo_sync(table_name,record_id,data,pushed_at)
    VALUES(?,?,?,?)`);
  const insertMany = db.transaction(recs => {
    for(const r of recs)
      stmt.run(table, r.id||`r_${Date.now()}`, JSON.stringify(r), new Date().toISOString());
  });
  insertMany(records);
  res.json({ok:true, count:records.length, table});
});

app.get('/gilrobo/sync/:table', (req,res) => {
  const rows = db.prepare(`SELECT data FROM gilrobo_sync WHERE table_name=? ORDER BY pushed_at DESC`)
               .all(req.params.table);
  res.json({ok:true, data:rows.map(r=>JSON.parse(r.data)), count:rows.length});
});

app.listen(process.env.PORT||3001, () => console.log('GilRobo Sync API running'));
Node.js

Python / Flask

# app.py — pip install flask flask-cors
import os, json, sqlite3
from flask import Flask, request, jsonify
from flask_cors import CORS
from datetime import datetime

app = Flask(__name__)
CORS(app)
API_KEY = os.getenv('GILROBO_KEY','change-me')
DB_PATH = 'gilrobo.db'

def get_db():
    con = sqlite3.connect(DB_PATH)
    con.execute("""CREATE TABLE IF NOT EXISTS gilrobo_sync(
        table_name TEXT, record_id TEXT, data TEXT,
        pushed_at TEXT, PRIMARY KEY(table_name,record_id))""")
    con.commit()
    return con

def auth():
    k = request.headers.get('X-API-Key') or request.headers.get('Authorization','').replace('Bearer ','')
    return k == API_KEY

@app.route('/gilrobo/ping')
def ping():
    if not auth(): return jsonify(error='Unauthorized'),401
    return jsonify(status='ok',time=datetime.utcnow().isoformat())

@app.route('/gilrobo/tables', methods=['POST'])
def tables():
    if not auth(): return jsonify(error='Unauthorized'),401
    return jsonify(created=True, table=request.json.get('table'))

@app.route('/gilrobo/sync/<table>', methods=['PUT'])
def push(table):
    if not auth(): return jsonify(error='Unauthorized'),401
    records = request.json.get('data',[])
    con = get_db()
    for r in records:
        con.execute("INSERT OR REPLACE INTO gilrobo_sync VALUES(?,?,?,?)",
            (table, r.get('id',f"r_{datetime.utcnow().timestamp()}"),
             json.dumps(r), datetime.utcnow().isoformat()))
    con.commit()
    return jsonify(ok=True, count=len(records), table=table)

@app.route('/gilrobo/sync/<table>', methods=['GET'])
def pull(table):
    if not auth(): return jsonify(error='Unauthorized'),401
    con = get_db()
    rows = con.execute("SELECT data FROM gilrobo_sync WHERE table_name=? ORDER BY pushed_at DESC",(table,)).fetchall()
    data = [json.loads(r[0]) for r in rows]
    return jsonify(ok=True, data=data, count=len(data))

if __name__ == '__main__':
    app.run(port=3001)
Python

🔐Security Best Practices

⚠️ Important
API keys are stored in browser localStorage. Use HTTPS always, rotate keys regularly, and never use a production DB key in a publicly shared HTML file.

Checklist

Always use HTTPS
All API endpoints must be served over HTTPS. Never use plain HTTP for sync — credentials in headers would be visible.
Scoped API keys
Create a dedicated API key with write access only to the gilrobo_sync table. Never use a root/admin database credential.
CORS restrictions
In production, restrict Access-Control-Allow-Origin to only the domain serving GilRobo (e.g. https://yourschool.com). Wildcard * is fine for testing only.
Rate limiting
Add rate limiting to your API (e.g. 100 req/min per key). Azure API Management, AWS API Gateway, and WordPress plugins all support this natively.
Data sanitization
The sample backends above store records as JSON blobs — they do not execute or interpret the data. Still, validate that incoming payloads are arrays of objects and that individual record sizes are reasonable (<1 MB).
Personal data (GDPR/FERPA)
Student names, ages, quiz scores, and consultation transcripts are personal data. Ensure your cloud DB is hosted in a compliant region and that your privacy policy covers AI-assisted educational records. Consider pseudonymizing records before push if full GDPR compliance is required.

🔧Troubleshooting

ProblemCauseFix
Test Connection → "Failed to fetch"CORS not configured / wrong URLCheck API URL has no trailing slash. Add CORS headers to your API. Open browser console to see exact error.
HTTP 401 UnauthorizedWrong API keyDouble-check key has no spaces. WordPress: re-generate Application Password. Azure: copy Function Key, not Host Key.
HTTP 404 on sync routesRoute not registeredWordPress: deactivate and re-activate plugin. Azure/AWS: check route pattern in function.json / API Gateway.
Pull returns empty dataTable empty or wrong table namePush first, then pull. Check DB directly — query SELECT * FROM gilrobo_sync WHERE table_name='gs_students'
Data not appearing after pullRecords have no id fieldAll records must have an id string field for the merge to work. Add one if missing in your DB.
Auto-sync not triggeringTab was closed / interval clearedAuto-sync runs in-page only. For server-side scheduled sync, use a cron job that calls your own API.
WordPress nonce errorsNonce expired (24h)Use Application Passwords (permanent) instead of nonces for long-lived sessions.
DynamoDB scan slow on large dataFull table scan per pullAdd a GSI on pk (table_name) for faster queries, or migrate to RDS for SQL-style indexed access.

Reading the Sync Log

Open ☁️ Cloud Sync → Sync Log to see all push/pull operations with timestamps. Green = push success, Blue = pull success, Red = error.

💡 Need help?
Check browser DevTools → Network tab when a sync operation runs. The request URL, response body, and status code will tell you exactly what's happening between the app and your API.
Shopping Cart