Anyone else use LocalStorage as an alternative solution for offline mode in the free tier?

Keen to hear some experiences with this. I have been using this feature with capturing field data for wildlife surveys where service is unreliable in remote areas. It’s a great work around where I’ve been able to cache all my lookup table data for forms and save survey data while offline with a simple click to upload from LocalStorage back to the db once back online. Here’s a sample of my code for anyone interested, hope it helps!

  1. Save Record to LocalStorage from mobile app form:
let inputStr = txtDateTime.value; // "Saturday 21 March 2026 at 6:22 pm"

// Remove day name ("Saturday") since JS can't parse it
let cleanedStr = inputStr.replace(/^[A-Za-z]+ /, ""); // "21 March 2026 at 6:22 pm"

// Replace ' at ' with a space so JS can parse it
cleanedStr = cleanedStr.replace(" at ", " "); // "21 March 2026 6:22 pm"

// Parse with Date
let parsedDate = new Date(cleanedStr);

// Validate
if (isNaN(parsedDate.getTime())) {
  throw new Error("Still invalid date: " + cleanedStr);
}

// Convert to ISO string (Postgres-friendly)
const startDateForDB = parsedDate.toISOString();

console.log("Parsed ISO date:", startDateForDB);

const existing = Array.isArray(localStorage.values.cached_Surveys)
  ? localStorage.values.cached_Surveys
  : [];

// ✅ AUTO-INCREMENT ID
const nextId = existing.length > 0
  ? Math.max(...existing.map(x => x.id || 0)) + 1
  : 1;

// 2. New record
const newSurvey = {
  // Meta data
  id: nextId,
  Collector: current_user.fullName,
  date: getCurDateTime.data.brisbaneISO,
  formatteddate: txtDateTime.value,
  createdAt: new Date().toISOString(),
  synced: false,

  // Captured location data
  latitude: numLatitude.value,
  longitude: numLongitude.value,
  northing: varConvertUTM.value.northing,
  easting: varConvertUTM.value.easting,
  zone: varConvertUTM.value.zone,
  precision: numPrecision.value,
  
  // Species data
  commonname: selSpecies.labels[selSpecies.values.indexOf(selSpecies.value)],
  speciescode: selSpecies.value,

  // Bio data
  agecode: selAge.value,
  agedescr: selAge.labels[selAge.values.indexOf(selAge.value)],
  
  breedingcode: selBreeding.value,
  breedingdescr: selBreeding.labels[selBreeding.values.indexOf(selBreeding.value)],
  
  animalcount: numCount.value,
  
  countcode: selCountCode.value,
  countdescr: selCountCode.labels[selCountCode.values.indexOf(selCountCode.value)],

  datumcode: selDatum.value,
  datumdescr: selDatum.labels[selDatum.values.indexOf(selDatum.value)],
  
  identificationcode: selIdentification.value,
  identificationdescr: selIdentification.labels[selIdentification.values.indexOf(selIdentification.value)],

  landformcode: selLandform.value,
  landformdescr: selLandform.labels[selLandform.values.indexOf(selLandform.value)],
  
  locationdescr: selLocation.labels[selLocation.values.indexOf(selLocation.value)],
  locationcode: selLocation.value,
  
  sexcode: selSex.value,
  sexdescr: selSex.labels[selSex.values.indexOf(selSex.value)],
  
  vegetationcode: selVegetation.value,
  vegetationdescr: selVegetation.labels[selVegetation.values.indexOf(selVegetation.value)],

  vettingcode: selVetting.value,
  vettingdescr: selVetting.labels[selVetting.values.indexOf(selVetting.value)],
  
  notes: txtNotes.value
};

// 3. Push
existing.push(newSurvey);

// 4. Save (NO stringify)
localStorage.setValue("cached_Surveys", existing);

// 5. Return
return existing;
  1. Retrieve record for review mapping info while offline:
{{ 
  (localStorage.values.cached_Surveys || []).map(row => ({
    longitude: row.longitude,
    latitude: row.latitude,
    location: row.locationdescr,
    locationcode: row.locationcode,
    id: row.id,
    collector: row.Collector,
    formatteddate: row.formatteddate,
    date: row.date,
    vegetation: row.vegetationdescr,
    vegetationcode: row.vegetationcode,
    commonname: row.commonname,
    speciescode: row.speciescode,
    identification: row.identificationdescr,
    identificationcode: row.identificationcode,
    sex: row.sexcode,
    notes: row.notes
  }))
}}
  1. Sync data up to database as inserted records once survey session complete:
if (OnlineStatus.value === true) 
{
  // Get cached surveys
  const surveys = localStorage.values.cached_Surveys || [];
  
  // Only send unsynced ones (important)
  const unsynced = surveys.filter(x => !x.synced);
  
  // Map to match SQL columns
  return unsynced.map(s => ({
    // Prep meta data
    date: s.date,
    formatteddate: s.formatteddate,
    collector: s.Collector, 

    // Prep Location data
    latitude: s.latitude,
    longitude: s.longitude,
    northing: s.northing,
    easting: s.easting,
    zone: s.zone,
    precision: s.precision,

    // Prep Bio data
    agecode: s.agecode, 
    breedingcode: s.breedingcode,
    animalcount: s.animalcount,
    countcode: s.countcode,
    datumcode: s.datumcode,
    identificationcode: s.identificationcode,
    landformcode: s.landformcode,
    locationcode: s.locationcode,
    sexcode: s.sexcode,
    speciescode: s.speciescode, 
    vegetationcode: s.vegetationcode,
    vettingcode: s.vettingcode,
    notes: s.notes,
    startdate: s.date,
    surveylogid: txtLogId.value
    
  }));
}

PostgreSQL insert:

INSERT INTO "data.surveys" ("date", "collector", "latitude", "longitude", "age_code", "breeding_code", "animal_count", "count_code", "datum_code", "identification_code", "landform_code", "location_code", "sex_code", "species_code", "vegetation_code", "vetting_code", "notes", "start_date","log_id", "northing", "easting", "zone", "detectiondate_formatted","precision")
SELECT *
FROM jsonb_to_recordset(
  {{ JSON.stringify(prepSurveysForSync.data) }}
) AS x(
  "date" timestamptz,
  "collector" text,
  "latitude" float,
  "longitude" float,
  "agecode" text,
  "breedingcode" text,
  "animalcount" int,
  "countcode" text,
  "datumcode" text,
  "identificationcode" text,
  "landformcode" text,
  "locationcode" text,
  "sexcode" text,
  "speciescode" text,
  "vegetationcode" text,
  "vettingcode" text,
  "notes" text,
  "startdate" timestamptz,
  "surveylogid" text,
  "northing" float,
  "easting" float,
  "zone" int,
  "formatteddate" text,
  "precision" int
);
2 Likes

Hi, I started out on the free version and am now on the paid one; in both, we used the exact same approach as you, since we always have connection issues here. What exactly is your problem? Let me know so I can help you out.

1 Like

@Augustine_Whitehouse moving your topic to Discussion, others can chime in!

My issue is that when I am out conducting wildlife surveys, I’m often in remote areas with little to no service. I need the app to continue being functional with storing location data during these periods of time.

The free version doesn't have that functionality; you need to upgrade to a paid plan for it to work completely offline. Otherwise, keep the app open at all times to avoid losing your cached data.

1 Like

The cached data is persistent and there is even a setting to make it persistent even if you’ve logged out. I’ve tested it and found that LocalStorage does in fact keep the data in cache even if you have to restart the app. I have a Boolean variable called unSynced as well so that I can push data to my database when in service and continue to keep the data in cache for reference using the Boolean. I also have an admin function to simply clear the cache when I no longer need to maintain it.. e.g after a survey session