QC Cloud Variables

TurboWarp Extension for Cloud Key-Value Storage

API Documentation

View the full REST API documentation for advanced usage

Getting Started

This extension allows you to save and load data from the cloud in your TurboWarp projects. Perfect for saving high scores, user preferences, or sharing data between projects!

1
Open TurboWarp

Go to turbowarp.org/editor

2
Add the Extension

Click the "Extensions" button (puzzle piece icon) in the bottom-left corner

3
Load Custom Extension

Scroll down and click "Custom Extension", then either:

Extension Code
(function(Scratch) {
  'use strict';

  const apiBase = 'https://kvstore.quincycoders.click';
  const THROTTLE_MS = 1000;
  const lastCallTime = {};
  const cache = {};

  function throttle(key) {
    const now = Date.now();
    if (lastCallTime[key] && now - lastCallTime[key] < THROTTLE_MS) {
      return false;
    }
    lastCallTime[key] = now;
    return true;
  }

  function getCached(key, defaultValue) {
    return cache[key] !== undefined ? cache[key] : defaultValue;
  }

  function setCache(key, value) {
    cache[key] = value;
    return value;
  }

  class QCCloudVariables {
    getInfo() {
      return {
        id: 'qccloudvariables',
        name: 'QC cloud variables',
        blocks: [
          {
            opcode: 'getAllKeys',
            blockType: Scratch.BlockType.REPORTER,
            text: 'get all keys from [TOPIC]',
            arguments: {
              TOPIC: {
                type: Scratch.ArgumentType.STRING,
                defaultValue: 'myTopic'
              }
            }
          },
          {
            opcode: 'getValue',
            blockType: Scratch.BlockType.REPORTER,
            text: 'get [KEY] from [TOPIC]',
            arguments: {
              KEY: {
                type: Scratch.ArgumentType.STRING,
                defaultValue: 'myKey'
              },
              TOPIC: {
                type: Scratch.ArgumentType.STRING,
                defaultValue: 'myTopic'
              }
            }
          },
          {
            opcode: 'setValue',
            blockType: Scratch.BlockType.COMMAND,
            text: 'set [KEY] to [VALUE] in [TOPIC]',
            arguments: {
              KEY: {
                type: Scratch.ArgumentType.STRING,
                defaultValue: 'myKey'
              },
              VALUE: {
                type: Scratch.ArgumentType.STRING,
                defaultValue: 'myValue'
              },
              TOPIC: {
                type: Scratch.ArgumentType.STRING,
                defaultValue: 'myTopic'
              }
            }
          },
          {
            opcode: 'deleteKey',
            blockType: Scratch.BlockType.COMMAND,
            text: 'delete [KEY] from [TOPIC]',
            arguments: {
              KEY: {
                type: Scratch.ArgumentType.STRING,
                defaultValue: 'myKey'
              },
              TOPIC: {
                type: Scratch.ArgumentType.STRING,
                defaultValue: 'myTopic'
              }
            }
          }
        ]
      };
    }

    getAllKeys({ TOPIC }) {
      const cacheKey = `getAllKeys:${TOPIC}`;
      if (!throttle(cacheKey)) return getCached(cacheKey, '[]');
      const url = `${apiBase}/${encodeURIComponent(TOPIC)}`;
      return Scratch.fetch(url)
        .then(response => response.json())
        .then(data => setCache(cacheKey, JSON.stringify(data.keys || [])))
        .catch(() => getCached(cacheKey, '[]'));
    }

    getValue({ KEY, TOPIC }) {
      const cacheKey = `getValue:${TOPIC}:${KEY}`;
      if (!throttle(cacheKey)) return getCached(cacheKey, '');
      const url = `${apiBase}/${encodeURIComponent(TOPIC)}/${encodeURIComponent(KEY)}`;
      return Scratch.fetch(url)
        .then(response => response.json())
        .then(data => setCache(cacheKey, data.value || ''))
        .catch(() => getCached(cacheKey, ''));
    }

    setValue({ KEY, VALUE, TOPIC }) {
      if (!throttle(`setValue:${TOPIC}:${KEY}`)) return;
      const url = `${apiBase}/${encodeURIComponent(TOPIC)}/${encodeURIComponent(KEY)}/${encodeURIComponent(VALUE)}`;
      return Scratch.fetch(url, { method: 'PUT' }).catch(() => {});
    }

    deleteKey({ KEY, TOPIC }) {
      if (!throttle(`deleteKey:${TOPIC}:${KEY}`)) return;
      const url = `${apiBase}/${encodeURIComponent(TOPIC)}/${encodeURIComponent(KEY)}`;
      return Scratch.fetch(url, { method: 'DELETE' }).catch(() => {});
    }
  }

  Scratch.extensions.register(new QCCloudVariables());
})(Scratch);

Available Blocks

Reporter Get All Keys

get all keys from TOPIC

Get a list of all keys stored in a topic. Returns a JSON array of key names.

InputDescription
TOPICThe topic/namespace to list keys from

Reporter Get Value

get KEY from TOPIC

Retrieve a value from the cloud. Returns an empty string if the key doesn't exist.

InputDescription
KEYThe name of the value to retrieve
TOPICThe topic/namespace (like a folder for your data)

Command Set Value

set KEY to VALUE in TOPIC

Save a value to the cloud. The value will automatically expire after 24 hours.

InputDescription
KEYThe name for your value
VALUEThe value to store
TOPICThe topic/namespace (like a folder for your data)

Command Delete Key

delete KEY from TOPIC

Delete a value from the cloud.

InputDescription
KEYThe name of the value to delete
TOPICThe topic/namespace containing the key

Example: High Score System

Here's how to create a simple high score system:

when green flag clicked
set [current score v] to (get [highscore] from [mygame])
if <(current score) = []> then
  set [current score v] to [0]
end
say (join [High Score: ] (current score))

when I receive [game over v]
if <(score) > (current score)> then
  set [highscore] to (score) in [mygame]
  say [New High Score!]
end

Tips & Best Practices

Use unique topics! Choose a topic name that's unique to your project to avoid conflicts with other users. Consider using your username + project name, like alice-spacegame.
Data expires after 24 hours. This service is designed for temporary data storage. Don't rely on it for permanent storage.
Rate limit: Each block is throttled to 1 call per second per key. Rapid repeated calls will return the previously cached value instead of making a new request.

Troubleshooting

ProblemSolution
Get block returns empty The key might not exist or may have expired. Check spelling and topic name.
Data disappears Values expire after 24 hours automatically.
Extension not loading Make sure you're using TurboWarp. Custom extensions aren't supported on scratch.mit.edu.
Blocks are slow Network requests take time. Use "wait until" blocks or check the value in a loop.