Connect GilRobo to
Any Cloud Database
Push quizzes, lessons, parent day consultations, and student data to your cloud — and pull new admin-loaded data back into the app. Full two-way sync, four providers supported.
ℹ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.
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 / Key | Contents | Group |
|---|---|---|
gs_students | Classroom student roster, scores, quiz history | Classroom |
gs_quiz_results | All classroom quiz results with scores per student | Classroom |
gs_lectures | Classroom sessions — transcript, duration, attendance | Classroom |
gs_settings | Teacher config — name, subject, schedule, blocked topics | Classroom |
qm_quiz_history | History Quiz tab results with full question/answer data | Quiz |
qm_quiz2_history | IELTS / Quiz 2 results | Quiz |
qm_tutorial_history | Tutorial session history | Quiz |
qm_custom_topics | Admin-created custom topics (title + body) | Quiz |
qm_custom_questions | Admin-created custom question banks | Quiz |
gr_pd_history | Parent Day consultation transcripts | Parent Day |
pd_admin_settings | Parent Day blocked topics & admin rules | Parent Day |
Admin Workflow — Loading New Data
🏗System Architecture
localStorage
fetch() / REST
/wp-json/gilrobo/v1/
/api/gilrobo/
/prod/gilrobo/
any backend
WordPress DB
Azure
AWS
Custom
API Contract — all providers use the same 4 endpoints
| Method | Path | Description |
|---|---|---|
GET | /gilrobo/ping | Health check — returns {"status":"ok"} |
POST | /gilrobo/tables | Create 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
🔵WordPress Setup
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
- Click ☁️ (floating button, bottom-right of GilRobo app)
- Select provider: WordPress REST API
- Paste the REST URL into WordPress Site URL
- In WP Admin → Users → your user → Application Passwords, create a password and paste it (format:
base64(username:app-password)) - Click 🔌 Test Connection — should say "Connected ✅"
- Click 🗄️ Create Tables, then 🔁 Two-Way Sync
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
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
- Deploy the function app to Azure. Copy the Function URL (e.g.
https://myapp.azurewebsites.net/api) - In GilRobo → ☁️ → Provider: Microsoft Azure
- Paste the function base URL (up to
/api) into Azure API Endpoint - Paste your Function Key (from Azure Portal → Function → Keys) into Azure Function Key
- Test, Create Tables, Sync
🟠Amazon AWS Setup
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
- Create a REST API in API Gateway
- Create resource
/gilrobo/{proxy+}with Lambda proxy integration - Enable API Key Required on the method and create an API key + usage plan
- Deploy to stage
prod - Copy the Invoke URL (e.g.
https://xxxx.execute-api.eu-west-2.amazonaws.com/prod)
Configure in GilRobo app
- Provider: Amazon AWS
- Paste invoke URL into API Gateway Endpoint
- Paste API Gateway key into API Gateway Key
- Test Connection → Create Tables → Sync
💜Microsoft Dataverse Setup
Architecture
login.microsoftonline.com
OData v4 / REST
cr_gilrobo_*
Step 1 — Register an Azure App
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 Key | Dataverse Table Name | Purpose |
|---|---|---|
gs_students | cr_gilrobogs_students | Student roster |
qm_quiz_history | cr_gilroboqmxquizxhistory | Quiz results |
gr_pd_history | cr_gilrobogr_pdxhistory | Parent Day transcripts |
gs_lectures | cr_gilrobogs_lectures | Classroom sessions |
For each table, create these columns in Power Apps → Tables → New table:
| Column Name | Type | Notes |
|---|---|---|
cr_recordid | Single line of text | Alternate key — must be unique |
cr_tablename | Single line of text | e.g. "gs_students" |
cr_data | Multiple lines of text | Full JSON record |
cr_pushedat | Date and time | Last push timestamp |
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
- Click 🗄️ Create Tables — this verifies each table entity is accessible
- Click 🔁 Two-Way Sync to do an initial full sync
- 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".
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
Checklist
gilrobo_sync table. Never use a root/admin database credential.Access-Control-Allow-Origin to only the domain serving GilRobo (e.g. https://yourschool.com). Wildcard * is fine for testing only.🔧Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| Test Connection → "Failed to fetch" | CORS not configured / wrong URL | Check API URL has no trailing slash. Add CORS headers to your API. Open browser console to see exact error. |
| HTTP 401 Unauthorized | Wrong API key | Double-check key has no spaces. WordPress: re-generate Application Password. Azure: copy Function Key, not Host Key. |
| HTTP 404 on sync routes | Route not registered | WordPress: deactivate and re-activate plugin. Azure/AWS: check route pattern in function.json / API Gateway. |
| Pull returns empty data | Table empty or wrong table name | Push first, then pull. Check DB directly — query SELECT * FROM gilrobo_sync WHERE table_name='gs_students' |
| Data not appearing after pull | Records have no id field | All records must have an id string field for the merge to work. Add one if missing in your DB. |
| Auto-sync not triggering | Tab was closed / interval cleared | Auto-sync runs in-page only. For server-side scheduled sync, use a cron job that calls your own API. |
| WordPress nonce errors | Nonce expired (24h) | Use Application Passwords (permanent) instead of nonces for long-lived sessions. |
| DynamoDB scan slow on large data | Full table scan per pull | Add 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.
