This document defines all storage keys, data structures, and patterns used by Solo for data persistence.
- Overview
- localStorage Schema
- sessionStorage Schema
- TypeScript Type Definitions
- Storage Patterns
- Data Migration
- Example Data
Solo uses the browser's Web Storage API for data persistence:
- localStorage: Persistent storage for folders, requests, variables, and theme
- sessionStorage: Temporary storage for current folder tracking
Storage Quota:
- localStorage: Typically 5-10MB per origin
- sessionStorage: Similar quota, cleared when tab/window closes
Data Format: All data is stored as JSON strings and parsed when retrieved.
Each folder is stored with the folder name as the key.
Key Pattern: {folderName}
Value Type: Array<StoredFile>
Type Definition:
type StoredFile = {
fileName: string; // Unique identifier (e.g., "request_1234567890")
fileData: RequestData; // Request configuration
displayName?: string; // Human-readable name (e.g., "Get Users")
};
type RequestData = {
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
url: string;
payload?: string; // JSON string
response?: unknown; // Last response data
useBasicAuth?: boolean;
username?: string;
password?: string;
bearerToken?: string;
activeTab?: "body" | "auth" | "params" | "graphql" | "grpc" | "proto" | "variables" | "description" | "schema";
queryParams?: QueryParam[];
requestType?: "http" | "graphql" | "grpc";
graphqlQuery?: string;
graphqlVariables?: string; // JSON string
grpcService?: string;
grpcMethod?: string;
grpcMessage?: string; // JSON string
grpcCallType?: "unary" | "server_streaming" | "client_streaming" | "bidirectional";
protoContent?: string;
description?: string;
};
type QueryParam = {
key: string;
value: string;
enabled: boolean;
};Example Key: "Work Projects"
Example Value:
[
{
"fileName": "request_1704459600000",
"displayName": "Get Users",
"fileData": {
"method": "GET",
"url": "https://api.example.com/users",
"payload": "",
"useBasicAuth": false,
"activeTab": "body",
"bearerToken": "",
"queryParams": [
{ "key": "limit", "value": "10", "enabled": true },
{ "key": "offset", "value": "0", "enabled": true }
],
"requestType": "http",
"description": "Fetches a paginated list of users"
}
},
{
"fileName": "request_1704460000000",
"displayName": "Create User",
"fileData": {
"method": "POST",
"url": "https://api.example.com/users",
"payload": "{\n \"name\": \"John Doe\",\n \"email\": \"john@example.com\"\n}",
"useBasicAuth": false,
"activeTab": "body",
"bearerToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"queryParams": [{ "key": "", "value": "", "enabled": true }],
"requestType": "http",
"description": "Creates a new user account"
}
}
]Notes:
- Each folder name must be unique
fileNameis generated usingrequest_${Date.now()}displayNamedefaults to request type if not provided- All optional fields may be omitted
Variables are stored per folder with a prefixed key pattern.
Key Pattern: solo-variables-{folderName}
Value Type: Array<Variable>
Type Definition:
type Variable = {
key: string; // Variable name (without braces)
value: string; // Variable value
enabled: boolean; // Whether to use this variable
};Example Key: "solo-variables-Work Projects"
Example Value:
[
{
"key": "baseUrl",
"value": "https://api.staging.example.com",
"enabled": true
},
{
"key": "apiKey",
"value": "sk_test_1234567890",
"enabled": true
},
{
"key": "version",
"value": "v2",
"enabled": false
}
]Usage in Requests:
URL: {{baseUrl}}/{{version}}/users
Result: https://api.staging.example.com/v2/users (if version is enabled)
Notes:
- Variables are scoped to folders
- Disabled variables are not replaced
- Variable keys are case-sensitive
- Supports whitespace inside braces:
{{ baseUrl }}is valid
Theme preference is stored with a simple key.
Key: "theme"
Value Type: "light" | "dark"
Example Value:
"dark"Default Behavior:
- If no theme is stored, detects system preference via
window.matchMedia("(prefers-color-scheme: dark)") - Falls back to
"light"if system preference unavailable
Tracks the currently active folder for variable loading.
Key: "current-request-folder"
Value Type: string (folder name)
Example Value:
"Work Projects"Usage:
- Set when a folder is selected or a request is opened
- Used to restore variable context on page refresh
- Cleared when session ends (tab/window close)
Notes:
- This is the only sessionStorage key used by Solo
- Helps maintain context between page reloads during development
Complete type definitions for reference:
// FileContext types
type RequestData = {
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
url: string;
payload?: string;
response?: unknown;
useBasicAuth?: boolean;
username?: string;
password?: string;
bearerToken?: string;
activeTab?: "body" | "auth" | "params" | "graphql" | "grpc" | "proto" | "variables" | "description" | "schema";
queryParams?: QueryParam[];
requestType?: "http" | "graphql" | "grpc";
graphqlQuery?: string;
graphqlVariables?: string;
grpcService?: string;
grpcMethod?: string;
grpcMessage?: string;
grpcCallType?: "unary" | "server_streaming" | "client_streaming" | "bidirectional";
protoContent?: string;
description?: string;
};
type StoredFile = {
fileName: string;
fileData: RequestData;
displayName?: string;
};
type QueryParam = {
key: string;
value: string;
enabled: boolean;
};
// VariablesContext types
type Variable = {
key: string;
value: string;
enabled: boolean;
};
// ThemeContext types
type Theme = "light" | "dark";When loading folders, skip variable storage keys:
const loadAllFolders = () => {
const loadedFolders: FolderStructure = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
// Skip variable keys and other non-folder keys
if (key && !key.startsWith("solo-variables-") && key !== "theme") {
try {
const files = JSON.parse(localStorage.getItem(key) || "[]") as StoredFile[];
loadedFolders[key] = files.map(file => file.fileName);
} catch {
continue; // Skip invalid JSON
}
}
}
return loadedFolders;
};Always use the helper function for consistency:
const getVariablesStorageKey = (folderName: string) => {
return `solo-variables-${folderName}`;
};
// Usage
const storageKey = getVariablesStorageKey("My Folder");
// Result: "solo-variables-My Folder"When renaming a folder, migrate both folder and variable data:
const renameFolder = (oldName: string, newName: string) => {
// 1. Get old data
const oldFolderData = localStorage.getItem(oldName);
const oldVariablesData = localStorage.getItem(`solo-variables-${oldName}`);
// 2. Write to new keys
if (oldFolderData) {
localStorage.setItem(newName, oldFolderData);
}
if (oldVariablesData) {
localStorage.setItem(`solo-variables-${newName}`, oldVariablesData);
}
// 3. Clean up old keys
localStorage.removeItem(oldName);
localStorage.removeItem(`solo-variables-${oldName}`);
};Remove both folder and variable storage:
const removeFolder = (folder: string) => {
localStorage.removeItem(folder);
localStorage.removeItem(`solo-variables-${folder}`);
};Use React useEffect to auto-save when request fields change:
useEffect(() => {
if (currentRequestId && currentFolder) {
saveCurrentRequest();
}
}, [method, url, payload, /* ...other fields */]);Current Version: No versioning system implemented yet
Future Considerations:
- Add a
solo-schema-versionkey to track storage schema version - Implement migration functions for breaking changes
- Consider adding a
migratedflag to each folder/request
Example Migration Pattern:
const CURRENT_SCHEMA_VERSION = 1;
const migrateStorage = () => {
const storedVersion = parseInt(localStorage.getItem("solo-schema-version") || "0");
if (storedVersion < CURRENT_SCHEMA_VERSION) {
// Run migrations
if (storedVersion === 0) {
// Migrate from v0 to v1
migrateV0ToV1();
}
localStorage.setItem("solo-schema-version", CURRENT_SCHEMA_VERSION.toString());
}
};- Never change key patterns without migration
- Never remove required fields from types
- Always make new fields optional for backwards compatibility
- Always validate JSON before parsing to prevent crashes
// Folder: "Personal Projects"
localStorage.setItem("Personal Projects", JSON.stringify([
{
"fileName": "request_1704459600000",
"displayName": "GitHub API - Get User",
"fileData": {
"method": "GET",
"url": "https://api.github.com/users/{{username}}",
"payload": "",
"useBasicAuth": false,
"activeTab": "params",
"bearerToken": "ghp_1234567890",
"queryParams": [
{ "key": "per_page", "value": "30", "enabled": true }
],
"requestType": "http",
"description": "Fetches GitHub user profile"
}
}
]));
// Variables for "Personal Projects"
localStorage.setItem("solo-variables-Personal Projects", JSON.stringify([
{
"key": "username",
"value": "octocat",
"enabled": true
},
{
"key": "repo",
"value": "hello-world",
"enabled": true
}
]));
// Theme
localStorage.setItem("theme", JSON.stringify("dark"));{
"fileName": "request_1704460000000",
"displayName": "GraphQL - Get Posts",
"fileData": {
"method": "POST",
"url": "https://api.example.com/graphql",
"payload": "",
"useBasicAuth": false,
"activeTab": "graphql",
"bearerToken": "",
"queryParams": [{ "key": "", "value": "", "enabled": true }],
"requestType": "graphql",
"graphqlQuery": "query GetPosts($limit: Int!) {\n posts(limit: $limit) {\n id\n title\n author {\n name\n }\n }\n}",
"graphqlVariables": "{\n \"limit\": 10\n}",
"description": "Fetches posts with author information"
}
}{
"fileName": "request_1704470000000",
"displayName": "gRPC - List Users",
"fileData": {
"method": "POST",
"url": "localhost:50051",
"payload": "",
"useBasicAuth": false,
"activeTab": "grpc",
"bearerToken": "",
"queryParams": [{ "key": "", "value": "", "enabled": true }],
"requestType": "grpc",
"grpcService": "UserService",
"grpcMethod": "ListUsers",
"grpcMessage": "{\n \"page\": 1,\n \"pageSize\": 20\n}",
"grpcCallType": "unary",
"protoContent": "syntax = \"proto3\";\n\nservice UserService {\n rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);\n}\n\nmessage ListUsersRequest {\n int32 page = 1;\n int32 pageSize = 2;\n}\n\nmessage ListUsersResponse {\n repeated User users = 1;\n}\n\nmessage User {\n string id = 1;\n string name = 2;\n string email = 3;\n}",
"description": "Lists users with pagination"
}
}try {
const data = JSON.parse(localStorage.getItem(key) || "[]");
} catch (error) {
console.error("Invalid JSON in storage:", error);
// Fallback to default
return [];
}const isStoredFile = (obj: any): obj is StoredFile => {
return obj && typeof obj.fileName === "string" && typeof obj.fileData === "object";
};const files = JSON.parse(localStorage.getItem(folderName) || "[]") as StoredFile[];While not currently implemented, consider debouncing auto-save to reduce write frequency:
const debouncedSave = useDebounce(saveCurrentRequest, 500);if ('storage' in navigator && 'estimate' in navigator.storage) {
navigator.storage.estimate().then(estimate => {
const percentUsed = (estimate.usage / estimate.quota) * 100;
if (percentUsed > 90) {
console.warn("Storage quota nearly full!");
}
});
}Check:
- Verify key format:
solo-variables-{folderName}(exact match) - Check JSON validity in localStorage
- Ensure folder name matches exactly (case-sensitive)
- Verify
current-request-folderin sessionStorage
Check:
- Ensure key doesn't start with
solo-variables- - Verify JSON is valid array of
StoredFileobjects - Check for hidden characters in folder name
- Ensure folder name is not empty string
Check:
- Verify
currentRequestIdandcurrentFolderare set - Check useEffect dependencies include all request fields
- Ensure no console errors during save
- Verify localStorage is not disabled in browser
Check:
- Verify key is exactly
"theme" - Check value is
"light"or"dark"(with quotes in JSON) - Ensure localStorage write succeeded
- Check browser privacy settings allow localStorage