Chest Vars
API Reference
Almacená y leé variables de configuración desde cualquier lenguaje o plataforma. Sin archivos .env, sin configuraciones locales.
Chest Vars es un sistema de gestión centralizada de variables de configuración. En lugar de mantener archivos .env en cada servidor, guardás tus variables en Chest Vars y las leés con un simple HTTP request.
Cómo funciona
Cada proyecto tiene un owner (nombre de usuario del propietario) y un nombre de proyecto. Las variables se identifican por clave y están protegidas por alguno de los tres métodos de autenticación disponibles.
Cada proyecto usa uno de tres métodos de autenticación. El método se configura al crear el proyecto en el panel web.
Bearer Token (recomendado)
Incluir en el header de cada request:
Authorization: Bearer tu_token_aqui
Password
Incluir en el body del request:
{
"owner": "nombre_usuario",
"project": "nombre_proyecto",
"user": "usuario_proyecto",
"password": "contraseña_proyecto"
}
JWT (HS256)
Firmar un JWT con el secret del proyecto e incluirlo como Bearer token:
{
"owner": "nombre_usuario",
"project": "nombre_proyecto",
"iat": 1700000000
}
Todas las respuestas incluyen un campo ok. Si es false, el campo error contiene la descripción.
{ "ok": false, "error": "Token inválido o expirado" }
| Código | Significado |
|---|---|
| 200 | OK — request exitoso |
| 400 | Bad Request — faltan campos o son inválidos |
| 401 | Unauthorized — credenciales incorrectas |
| 403 | Forbidden — sin permisos para esta operación |
| 404 | Not Found — proyecto o variable no encontrada |
| 429 | Too Many Requests — rate limit excedido |
| 500 | Server Error — error interno |
Lee variables de un proyecto. Si se omite keys, devuelve todas las variables del proyecto.
Body
| Campo | Tipo | Descripción | |
|---|---|---|---|
| owner | string | requerido | Username del dueño del proyecto |
| project | string | requerido | Nombre del proyecto |
| keys | array | opcional | Lista de claves a leer. Sin este campo devuelve todas. |
Request
{
"owner": "miusuario",
"project": "mi-proyecto",
"keys": ["DB_HOST", "DB_PORT", "API_KEY"]
}
Response
{
"ok": true,
"data": {
"DB_HOST": "db.example.com",
"DB_PORT": "5432",
"API_KEY": "sk-abc123..."
}
}
Crea o actualiza una o más variables. Si la clave ya existe, actualiza el valor.
Body
| Campo | Tipo | Descripción | |
|---|---|---|---|
| owner | string | requerido | Username del dueño del proyecto |
| project | string | requerido | Nombre del proyecto |
| data | object | requerido | Objeto clave:valor con las variables a guardar |
Request
{
"owner": "miusuario",
"project": "mi-proyecto",
"data": {
"DB_HOST": "nuevo-host.com",
"VERSION": "2.1.0"
}
}
Response
{
"ok": true,
"updated": ["DB_HOST", "VERSION"],
"errors": {}
}
<?php
// Chest Vars – Cliente PHP
// Requiere PHP 7.4+ con extensión curl o allow_url_fopen=On
function chestvars_get(
string $owner,
string $project,
string $token,
array $keys = []
): array {
$body = ['owner' => $owner, 'project' => $project];
if (!empty($keys)) $body['keys'] = $keys;
$ch = curl_init('https://chestvars.com/api/get');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $token,
],
]);
$response = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
if ($error) throw new RuntimeException('cURL error: ' . $error);
$data = json_decode($response, true);
if (!$data['ok']) throw new RuntimeException($data['error'] ?? 'Error desconocido');
return $data['data'];
}
// Uso
$vars = chestvars_get(
owner: 'miusuario',
project: 'mi-proyecto',
token: 'mi_bearer_token',
keys: ['DB_HOST', 'DB_PORT', 'REDIS_URL']
);
echo $vars['DB_HOST']; // db.example.com
echo $vars['DB_PORT']; // 5432
import requests
def chestvars_get(owner, project, token, keys=None):
"""Lee variables de Chest Vars."""
body = {"owner": owner, "project": project}
if keys:
body["keys"] = keys
response = requests.post(
"https://chestvars.com/api/get",
json=body,
headers={"Authorization": f"Bearer {token}"},
timeout=10,
)
response.raise_for_status()
data = response.json()
if not data.get("ok"):
raise ValueError(data.get("error", "Error desconocido"))
return data["data"]
def chestvars_set(owner, project, token, variables: dict):
"""Guarda variables en Chest Vars."""
response = requests.post(
"https://chestvars.com/api/set",
json={"owner": owner, "project": project, "data": variables},
headers={"Authorization": f"Bearer {token}"},
timeout=10,
)
response.raise_for_status()
return response.json()
# Uso
TOKEN = "mi_bearer_token"
OWNER = "miusuario"
PROJECT = "mi-proyecto"
vars = chestvars_get(OWNER, PROJECT, TOKEN, keys=["DB_HOST", "API_KEY"])
print(vars["DB_HOST"]) # db.example.com
# Guardar
chestvars_set(OWNER, PROJECT, TOKEN, {"VERSION": "3.0.0"})
const CHESTVARS = {
BASE_URL: 'https://chestvars.com/api',
TOKEN: 'mi_bearer_token',
OWNER: 'miusuario',
PROJECT: 'mi-proyecto',
};
async function cvGet(keys = []) {
const body = {
owner: CHESTVARS.OWNER,
project: CHESTVARS.PROJECT,
...(keys.length && { keys }),
};
const res = await fetch(`${CHESTVARS.BASE_URL}/get`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CHESTVARS.TOKEN}`,
},
body: JSON.stringify(body),
});
const data = await res.json();
if (!data.ok) throw new Error(data.error);
return data.data;
}
async function cvSet(variables) {
const res = await fetch(`${CHESTVARS.BASE_URL}/set`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CHESTVARS.TOKEN}`,
},
body: JSON.stringify({
owner: CHESTVARS.OWNER,
project: CHESTVARS.PROJECT,
data: variables,
}),
});
const data = await res.json();
if (!data.ok) throw new Error(data.error);
return data;
}
// Uso
const vars = await cvGet(['DB_HOST', 'STRIPE_KEY']);
console.log(vars.DB_HOST);
await cvSet({ VERSION: '2.0.0', DEPLOYED_AT: new Date().toISOString() });
// npm install axios
const axios = require('axios');
const client = axios.create({
baseURL: 'https://chestvars.com/api',
headers: { Authorization: 'Bearer mi_bearer_token' },
timeout: 10000,
});
async function cvGet(owner, project, keys = []) {
const { data } = await client.post('/get.php', {
owner, project, ...(keys.length && { keys }),
});
if (!data.ok) throw new Error(data.error);
return data.data;
}
// Uso
const vars = await cvGet('miusuario', 'mi-proyecto', ['DB_HOST']);
console.log(vars.DB_HOST);
Imports System.Net.Http
Imports System.Text
Imports System.Text.Json
' Requiere .NET 5+ o .NET Framework 4.5+ con Newtonsoft.Json
Module ChestVarsClient
Private Const BaseUrl As String = "https://chestvars.com/api"
Private Const Token As String = "mi_bearer_token"
Private Const Owner As String = "miusuario"
Private Const Project As String = "mi-proyecto"
''' <summary>
''' Lee variables de Chest Vars.
''' </summary>
Public Async Function GetVariables(
ParamArray keys() As String
) As Task(Of Dictionary(Of String, String))
Using client As New HttpClient()
client.DefaultRequestHeaders.Add("Authorization", "Bearer " & Token)
Dim body As New Dictionary(Of String, Object) From {
{"owner", Owner},
{"project", Project}
}
If keys.Length > 0 Then body.Add("keys", keys)
Dim json = JsonSerializer.Serialize(body)
Dim content = New StringContent(json, Encoding.UTF8, "application/json")
Dim response = Await client.PostAsync(BaseUrl & "/get", content)
Dim raw = Await response.Content.ReadAsStringAsync()
Dim result = JsonSerializer.Deserialize(Of ApiResponse)(raw)
If Not result.ok Then Throw New Exception(result.error)
Return result.data
End Using
End Function
''' <summary>
''' Guarda variables en Chest Vars.
''' </summary>
Public Async Function SetVariables(
variables As Dictionary(Of String, String)
) As Task
Using client As New HttpClient()
client.DefaultRequestHeaders.Add("Authorization", "Bearer " & Token)
Dim body As New Dictionary(Of String, Object) From {
{"owner", Owner},
{"project", Project},
{"data", variables}
}
Dim json = JsonSerializer.Serialize(body)
Dim content = New StringContent(json, Encoding.UTF8, "application/json")
Dim response = Await client.PostAsync(BaseUrl & "/set", content)
Dim raw = Await response.Content.ReadAsStringAsync()
Dim result = JsonSerializer.Deserialize(Of ApiResponse)(raw)
If Not result.ok Then Throw New Exception(result.error)
End Using
End Function
Private Class ApiResponse
Public Property ok As Boolean
Public Property data As Dictionary(Of String, String)
Public Property [error] As String
End Class
End Module
' ── Uso desde un Form ──────────────────────────────────────
' Dim vars = Await ChestVarsClient.GetVariables("DB_HOST", "API_KEY")
' TextBox1.Text = vars("DB_HOST")
'
' Await ChestVarsClient.SetVariables(New Dictionary(Of String, String) From {
' {"VERSION", "2.0.0"}
' })
Leer variables
curl -X POST https://chestvars.com/api/get \
-H "Content-Type: application/json" \
-H "Authorization: Bearer mi_bearer_token" \
-d '{
"owner": "miusuario",
"project": "mi-proyecto",
"keys": ["DB_HOST", "DB_PORT"]
}'
Escribir variables
curl -X POST https://chestvars.com/api/set \
-H "Content-Type: application/json" \
-H "Authorization: Bearer mi_bearer_token" \
-d '{
"owner": "miusuario",
"project": "mi-proyecto",
"data": {
"VERSION": "2.1.0",
"UPDATED_AT": "2025-01-01T00:00:00Z"
}
}'
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
public class ChestVarsClient
{
private readonly HttpClient _http;
private readonly string _owner;
private readonly string _project;
public ChestVarsClient(string token, string owner, string project)
{
_owner = owner;
_project = project;
_http = new HttpClient
{
BaseAddress = new Uri("https://chestvars.com/api/")
};
_http.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");
}
public async Task<Dictionary<string, string>> GetAsync(params string[] keys)
{
var body = new { owner = _owner, project = _project, keys };
var res = await _http.PostAsJsonAsync("get", body);
var data = await res.Content.ReadFromJsonAsync<ApiResponse>();
if (!data!.Ok)
throw new Exception(data.Error ?? "Error desconocido");
return data.Data ?? new();
}
public async Task SetAsync(Dictionary<string, string> variables)
{
var body = new { owner = _owner, project = _project, data = variables };
var res = await _http.PostAsJsonAsync("set", body);
var data = await res.Content.ReadFromJsonAsync<ApiResponse>();
if (!data!.Ok)
throw new Exception(data.Error ?? "Error desconocido");
}
private record ApiResponse(
bool Ok,
Dictionary<string, string>? Data,
string? Error
);
}
// Uso
var cv = new ChestVarsClient("mi_token", "miusuario", "mi-proyecto");
var vars = await cv.GetAsync("DB_HOST", "REDIS_URL");
Console.WriteLine(vars["DB_HOST"]);
await cv.SetAsync(new() { ["VERSION"] = "3.0.0" });
El ESP32 puede conectarse a Chest Vars via HTTPS usando la librería HTTPClient incluida en el Arduino ESP32 core. Requiere conexión WiFi y un certificado raíz para validar TLS.
ArduinoJson — instalable desde el Library Manager de Arduino IDE buscando "ArduinoJson" de Benoit Blanchon. Versión 6 o superior.
Leer variables al iniciar
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
// ── Configuración ──────────────────────────────────────────
const char* WIFI_SSID = "tu_red_wifi";
const char* WIFI_PASSWORD = "tu_clave_wifi";
const char* CV_TOKEN = "tu_bearer_token";
const char* CV_OWNER = "miusuario";
const char* CV_PROJECT = "mi-proyecto";
const char* CV_URL = "https://chestvars.com/api/get";
// Variables leídas de Chest Vars
String dbHost;
String apiKey;
// ── Certificado raíz de chestvars.com ─────────────────────
// Obtenerlo con: openssl s_client -connect chestvars.com:443 -showcerts
// Pegá el certificado raíz (último de la cadena) aquí
const char* ROOT_CA = R"(
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
... (pegá el certificado completo aquí) ...
-----END CERTIFICATE-----
)";
// ── Función principal: leer variables ─────────────────────
bool chestvars_get(const char* keys[], int keyCount) {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("[ChestVars] Sin WiFi");
return false;
}
// Armar el JSON del body
StaticJsonDocument<256> doc;
doc["owner"] = CV_OWNER;
doc["project"] = CV_PROJECT;
JsonArray keysArr = doc.createNestedArray("keys");
for (int i = 0; i < keyCount; i++) {
keysArr.add(keys[i]);
}
String body;
serializeJson(doc, body);
// Hacer el request
HTTPClient http;
http.begin(CV_URL, ROOT_CA);
http.addHeader("Content-Type", "application/json");
http.addHeader("Authorization", String("Bearer ") + CV_TOKEN);
int httpCode = http.POST(body);
if (httpCode != 200) {
Serial.printf("[ChestVars] Error HTTP: %d\n", httpCode);
http.end();
return false;
}
// Parsear respuesta
String response = http.getString();
http.end();
StaticJsonDocument<1024> resp;
DeserializationError error = deserializeJson(resp, response);
if (error || !resp["ok"].as<bool>()) {
Serial.println("[ChestVars] Error en respuesta");
Serial.println(response);
return false;
}
// Leer los valores
JsonObject data = resp["data"];
dbHost = data["DB_HOST"].as<String>();
apiKey = data["API_KEY"].as<String>();
Serial.println("[ChestVars] Variables cargadas OK");
Serial.printf(" DB_HOST: %s\n", dbHost.c_str());
return true;
}
// ── Setup ─────────────────────────────────────────────────
void setup() {
Serial.begin(115200);
// Conectar WiFi
Serial.printf("Conectando a %s...\n", WIFI_SSID);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi conectado: " + WiFi.localIP().toString());
// Leer variables de Chest Vars
const char* keys[] = {"DB_HOST", "API_KEY"};
if (!chestvars_get(keys, 2)) {
Serial.println("Error al leer configuración. Reiniciando...");
delay(3000);
ESP.restart();
}
// Usar los valores
Serial.println("Config cargada. Arrancando app...");
}
void loop() {
// Tu código aquí — dbHost y apiKey ya están disponibles
}
Sin validación TLS (desarrollo)
Si estás probando en red local o no tenés el certificado, podés deshabilitar la validación TLS. No usar en producción.
// Reemplazar http.begin(CV_URL, ROOT_CA) por: http.begin(CV_URL); http.setInsecure(); // ← deshabilita verificación SSL
Obtener el certificado raíz
Para obtener el certificado correcto, ejecutá este comando en tu terminal y copiá el último certificado de la cadena (el raíz):
openssl s_client -connect chestvars.com:443 -showcerts 2>/dev/null \
| awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/' \
| tail -n +$(grep -c "BEGIN CERTIFICATE" <(openssl s_client \
-connect chestvars.com:443 -showcerts 2>/dev/null))
StaticJsonDocument con el tamaño justo para tu respuesta. Si tenés muchas variables, aumentá el tamaño o leelas en lotes. Podés calcular el tamaño exacto en arduinojson.org/v6/assistant.
Integración completa para apps Flutter. Incluye lectura, escritura y un cliente reutilizable con soporte de Bearer token.
http: ^1.2.0 y characters: ^1.3.0 — agregá ambas a tu pubspec.yaml y corré flutter pub get.
Cliente reutilizable
import 'dart:convert';
import 'package:characters/characters.dart';
import 'package:http/http.dart' as http;
class ChestVars {
static const String _base = 'https://chestvars.com/api';
static const String _owner = 'miusuario';
static const String _project = 'mi-proyecto';
static const String _token = 'mi_bearer_token';
static Map<String, String> get _headers => {
'Content-Type': 'application/json',
'Authorization': 'Bearer $_token',
};
// ── Leer variables ────────────────────────────────────────
static Future<Map<String, String>> get({List<String>? keys}) async {
final body = <String, dynamic>{
'owner': _owner,
'project': _project,
if (keys != null) 'keys': keys,
};
final res = await http.post(
Uri.parse('$_base/get'),
headers: _headers,
body: jsonEncode(body),
);
final data = jsonDecode(res.body) as Map<String, dynamic>;
if (data['ok'] != true) throw Exception(data['error']);
return Map<String, String>.from(data['data'] as Map);
}
// ── Guardar variables ─────────────────────────────────────
static Future<void> set(Map<String, String> variables) async {
// Enmascarar valores: reverse unicode-safe + base64
final encoded = variables.map((k, v) {
final reversed = v.characters.toList().reversed.join();
final masked = base64Encode(utf8.encode(reversed));
return MapEntry(k, masked);
});
final body = {
'owner': _owner,
'project': _project,
'encoded': true,
'data': encoded,
};
final res = await http.post(
Uri.parse('$_base/set'),
headers: _headers,
body: jsonEncode(body),
);
final data = jsonDecode(res.body) as Map<String, dynamic>;
if (data['ok'] != true) throw Exception(data['error']);
}
}
Leer variables al iniciar la app
// En main() o en initState() de tu widget inicial
Future<void> _loadConfig() async {
try {
final vars = await ChestVars.get(keys: ['API_URL', 'FEATURE_FLAGS', 'TIMEOUT']);
final apiUrl = vars['API_URL'] ?? 'https://default.example.com';
final timeout = int.tryParse(vars['TIMEOUT'] ?? '30') ?? 30;
final flagsJson = vars['FEATURE_FLAGS'] ?? '{}';
print('Config cargada: $apiUrl, timeout: ${timeout}s');
} catch (e) {
print('Error al cargar config: $e');
// Usar valores por defecto si falla
}
}
Guardar variables desde la app
// Guardar múltiples variables de una vez
await ChestVars.set({
'LAST_SYNC': DateTime.now().toIso8601String(),
'APP_VERSION': '2.1.0',
'USER_PREFS': jsonEncode({'theme': 'dark', 'lang': 'es'}),
'EMOJI_TEST': '🎉 Funciona con emojis',
});
Provider o InheritedWidget para distribuir la config por toda la app.
Siempre que necesites más de una variable, leelas todas en un único request. Esto reduce la latencia significativamente comparado con requests individuales.
// ✅ Recomendado — un solo request
{
"owner": "miusuario",
"project": "mi-proyecto",
"keys": ["DB_HOST", "DB_PORT", "DB_NAME", "DB_USER", "DB_PASS"]
}
// ❌ Evitar — múltiples requests innecesarios
// GET DB_HOST → request 1
// GET DB_PORT → request 2
// GET DB_NAME → request 3
// ...
Omitir el campo keys devuelve todas las variables del proyecto de una sola vez.
Para proteger el servicio, existe un límite de requests por IP y por proyecto. Si superás el límite, la API devuelve HTTP 429 con el error correspondiente.
import requests
from functools import lru_cache
@lru_cache(maxsize=1)
def get_config():
"""Lee la config una sola vez y la cachea en memoria."""
return chestvars_get(OWNER, PROJECT, TOKEN)
# Al iniciar la app:
config = get_config()
db_host = config["DB_HOST"] # no hace un request nuevo