Como monetizas un producto auto-alojado? El usuario descarga tu binario, lo ejecuta en su propio servidor y tiene control total sobre la maquina. No hay ninguna pagina de facturacion SaaS que tenga que visitar. No hay ningun medidor de uso en segundo plano. Todo el producto funciona localmente, y cualquier persona suficientemente motivada podria parchear el binario para eliminar tus verificaciones de licencia.
Esta es la tension fundamental de la monetizacion de software auto-alojado, y dedicamos una cantidad significativa de tiempo pensando en ello antes de escribir una sola linea de codigo de aplicacion de licencias. La respuesta a la que llegamos no fue tecnica. Fue filosofica: hacer que el nivel gratuito sea tan generoso que la mayoria de los usuarios nunca necesiten actualizar, y hacer que los niveles de pago sean lo suficientemente valiosos como para que los usuarios que si los necesitan esten contentos de pagar.
La filosofia de precios
Teniamos tres principios:
Principio 1: Free debe ser genuinamente util. Un desarrollador individual deberia poder desplegar aplicaciones, usar la terminal, ejecutar tareas cron, configurar hooks de despliegue, establecer entornos de preview, gestionar claves API, desplegar stacks Docker Compose, usar el sistema IaC sh0.yaml y establecer un dominio personalizado para el panel -- todo sin pagar nada. Si tu nivel gratuito requiere una actualizacion para hacer trabajo basico de desarrollo, no es un nivel gratuito. Es una prueba.
Principio 2: Pro restringe funcionalidades operativas, no funcionalidades principales. El nivel Pro ($19/mes) desbloquea respaldos automatizados, monitoreo de tiempo activo, alertas, paginas de estado, analisis de salud de codigo y exportaciones de stackshot. Estas son funcionalidades que necesitas cuando estas ejecutando cargas de trabajo en produccion y te importa la fiabilidad. Un hobbyista desplegando un proyecto personal no necesita programacion automatizada de respaldos. Una startup ejecutando servicios orientados al cliente si.
Principio 3: Business restringe funcionalidades de equipo. El nivel Business ($97/mes) desbloquea RBAC, miembros de equipo, auto-escalado y clusters multi-servidor (BYOS). Estas son funcionalidades que importan cuando multiples personas gestionan la misma plataforma. Un desarrollador individual no necesita control de acceso basado en roles. Un equipo de cinco si.
La sesion de alineacion de planes
Tuvimos una sesion dedicada (17 de marzo) donde alineamos todo el codigo con un nuevo documento de planes (PLANS.md). El modelo anterior era demasiado restrictivo -- acceso a terminal, tareas cron, hooks de despliegue, entornos de preview y claves API estaban todos restringidos detras de Pro. Esto hacia que el nivel gratuito fuera casi inutil para trabajo de desarrollo real.
La sesion de alineacion toco 12 ubicaciones de codigo:
- Modelo Rust (
license.rs): Se eliminaron 5 metodos siempre-verdaderos que restringian funcionalidades que decidimos deberian ser gratuitas (allows_terminal,allows_cron,allows_hooks,allows_previews,allows_api_keys). Se anadieron 4 nuevos metodos para funcionalidades genuinamente Pro/Business (allows_status_pages,allows_code_health,allows_stackshots,allows_team_members). Se anadioaudit_retention_days()(Free = 7 dias, Pro = 90 dias, Business = 365 dias).
- Manejador de API: Se actualizo la respuesta de
GET /api/v1/settings/licensepara reflejar el nuevo conjunto de funcionalidades.
- Pagina de licencias del panel: Se reescribio la tabla de comparacion de planes con nuevos precios ($0 / $19 / $97) y el nuevo desglose de funcionalidades.
- Pagina de precios del sitio web: Se elimino el toggle de precios de por vida (decidimos en contra de las licencias de por vida), se reescribieron las tres tarjetas de plan.
- 5 archivos de localizacion i18n: Se actualizaron los mensajes de actualizacion en ingles, frances, espanol, portugues y suajili.
- Endpoints de checkout: Se actualizaron los montos de precio de Stripe y ZeroFee, se elimino el modo de pago de por vida.
Precios para mercados africanos
Los precios fueron establecidos deliberadamente pensando en los desarrolladores africanos. La mayoria de las plataformas PaaS cobran $10-25 por asiento por mes, o $50+ por funcionalidades de equipo. Nuestro Pro a $19 y Business a $97 son competitivos globalmente, pero tambien integramos ZeroFee -- una pasarela de pago de Mobile Money que soporta mas de 135 proveedores en mas de 18 paises africanos. Un desarrollador en Abidjan puede pagar sh0 Pro con Orange Money. Un equipo en Nairobi puede pagar con M-Pesa. Esta no es una funcionalidad cosmetica. En muchos mercados africanos, las tarjetas de credito internacionales son dificiles de obtener, y Mobile Money es el principal metodo de pago digital.
La capa de aplicacion
Con la filosofia establecida, la implementacion fue directa. Tres componentes: una variante de error, helpers compartidos y restricciones por manejador.
ApiError::LicenseRequired
rust#[derive(Debug)]
pub enum ApiError {
// ... existing variants
LicenseRequired {
message: String,
required_plan: String,
},
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
match self {
ApiError::LicenseRequired { message, required_plan } => {
let body = json!({
"error": {
"code": "LICENSE_REQUIRED",
"message": message,
"required_plan": required_plan,
}
});
(StatusCode::FORBIDDEN, Json(body)).into_response()
}
// ...
}
}
}La decision de diseno clave: la respuesta de error incluye required_plan como un campo estructurado. El frontend no tiene que adivinar que plan se necesita -- el backend se lo dice, y el modal de actualizacion puede mostrar el plan y precio correctos. Esto importa porque diferentes funcionalidades requieren diferentes planes. Los respaldos requieren Pro. La gestion de equipo requiere Business. La respuesta de error impulsa la interfaz directamente.
Helpers compartidos
rustpub fn require_plan(state: &AppState, plan: &str) -> Result<(), ApiError> {
let license = state.license.read();
match &*license {
Some(lic) if lic.plan.allows(plan) => Ok(()),
_ => Err(ApiError::LicenseRequired {
message: format!("This feature requires the {} plan or higher", plan),
required_plan: plan.to_string(),
}),
}
}
pub fn require_pro(state: &AppState) -> Result<(), ApiError> {
require_plan(state, "pro")
}
pub fn require_business(state: &AppState) -> Result<(), ApiError> {
require_plan(state, "business")
}Tres funciones. Una linea para llamar en cualquier manejador. La funcion require_plan es la forma general, y require_pro / require_business son envoltorios de conveniencia para los dos casos mas comunes. Antes de esta refactorizacion, el manejador nodes.rs tenia su propia copia local de la verificacion Business -- los helpers compartidos eliminaron esa duplicacion.
Aplicacion en manejadores
Anadir una restriccion de licencia a un manejador es una sola linea al inicio de la funcion:
rustpub async fn trigger_backup(
State(state): State<AppState>,
auth: AuthUser,
Json(req): Json<TriggerBackupRequest>,
) -> Result<Json<BackupResponse>> {
require_pro(&state)?; // One line. That's it.
// ... rest of the handler
}Anadimos restricciones a 10 archivos de manejadores cubriendo mas de 25 funciones:
| Manejador | Restriccion | Funcionalidades bloqueadas |
|---|---|---|
backups.rs | Pro | Las 10 operaciones de respaldo (activar, programar, listar, restaurar, eliminar) |
alerts.rs | Pro | Creacion de alertas |
uptime.rs | Pro | Creacion de verificaciones de tiempo activo |
export.rs | Pro | Exportaciones de stackshot |
projects.rs | Business | Anadir/actualizar/eliminar miembros de equipo; Free limitado a 1 stack |
scaling.rs | Business | Configuracion de auto-escalado |
team.rs | Business | Las 4 operaciones de equipo (invitar, listar, actualizar, eliminar) |
nodes.rs | Business | Todas las operaciones multi-servidor |
El limite de creacion de stacks merece tratamiento especial. En lugar de una restriccion booleana, usa una verificacion de conteo:
rustpub async fn create_project(
State(state): State<AppState>,
auth: AuthUser,
Json(req): Json<CreateProjectRequest>,
) -> Result<Json<ProjectResponse>> {
let count = Project::count(&state.pool)?;
let max = state.license.read()
.as_ref()
.map(|l| l.plan.max_stacks())
.unwrap_or(1); // Free = 1 stack
if count >= max {
return Err(ApiError::LicenseRequired {
message: format!("Free plan allows {} stack(s). Upgrade to create more.", max),
required_plan: "pro".to_string(),
});
}
// ...
}Los usuarios Free obtienen 1 stack (proyecto). Pro y Business obtienen ilimitados. Esto es suficiente para que un desarrollador individual despliegue una aplicacion significativa (un stack con multiples servicios), mientras hace el camino de actualizacion claro una vez que necesitan gestionar multiples proyectos.
Aplicacion en el panel
Las restricciones del lado del servidor previenen la operacion, pero una buena experiencia de usuario las detecta antes -- antes de la llamada API. El panel aplica restricciones de licencia en dos niveles.
Restricciones a nivel de pagina
Para paginas enteras que estan restringidas por plan (respaldos, monitoreo, equipo, nodos), la restriccion esta a nivel de pagina:
svelte<script>
import { isProOrAbove } from '$lib/stores/license';
import UpgradePrompt from '$lib/components/UpgradePrompt.svelte';
</script>
{#if isProOrAbove()}
<!-- Normal page content -->
{:else}
<UpgradePrompt plan="pro" feature="Automated Backups" />
{/if}El usuario ve la indicacion de actualizacion inmediatamente, sin hacer una llamada API que fallaria de todas formas.
Intercepcion de errores de API
Para operaciones dentro de una pagina (como crear un segundo stack en la pagina principal), la restriccion captura el error de API:
svelteasync function createStack() {
try {
await stacksApi.create(formData);
} catch (e) {
if (e.required_plan) {
showUpgradePrompt = true;
upgradePlan = e.required_plan;
} else {
toast.error(e.message);
}
}
}La clase ApiError en el frontend fue extendida con un campo required_plan. Tanto api() como apiRaw() lo extraen de la respuesta de error estructurada. La pagina de stacks usa e.required_plan ?? 'pro' para que el UpgradePrompt muestre el plan correcto dinamicamente.
El componente UpgradePrompt
El componente UpgradePrompt.svelte existia antes de la sesion de aplicacion -- fue creado durante la fase del sistema de licencias pero nunca se uso realmente. Muestra un modal con dos acciones: "Ver precios" (enlaza a sh0.dev/pricing) y "Ingresar clave de licencia" (navega a la pagina /license en el panel). La prop plan determina que plan resaltar y que precios mostrar.
Lo que deliberadamente dejamos sin aplicar
No todas las funcionalidades con asociacion a un plan se aplican a nivel de API. Algunas funcionalidades se auto-aplican:
- Retencion de auditoria: El metodo
audit_retention_days()devuelve 7/90/365 basado en el plan, pero ningun trabajo en segundo plano elimina los registros de auditoria antiguos todavia. Cuando se construya el podador, leera el plan y eliminara en consecuencia. - Expiracion de licencia: El campo
valid_untilexiste en el modelo de licencia pero nunca se verifica. Las licencias expiradas deberian degradar a Free, pero esto es una sesion futura.
Tampoco tenemos pruebas de aplicacion de licencias. Esta es una deuda consciente. Una refactorizacion podria eliminar silenciosamente una llamada a require_pro(), y ninguna prueba lo detectaria. Anadir pruebas de integracion que verifiquen que "un usuario Free obtiene 403 al activar respaldo" esta en la hoja de ruta.
El modelo de confianza auto-alojado
No ofuscamos las verificaciones de licencia. No llamamos a casa. No usamos servidores de licencia. La verificacion es una llamada de funcion en el manejador que lee un registro de licencia local de SQLite. Un usuario suficientemente motivado podria parchear el binario, modificar la base de datos o escribir una migracion que establezca su plan a "business".
Esto esta bien. El software auto-alojado opera en un modelo de confianza. Los usuarios que parchearian el binario no son los usuarios que pagarian por una licencia. Los usuarios que pagan son equipos y empresas que quieren soporte oficial, actualizaciones oportunas y la confianza de que estan ejecutando un producto mantenido. La aplicacion de licencias existe para hacer el modelo de negocio legible -- para dejar claro que incluye cada nivel y para indicar la actualizacion en el momento adecuado -- no para hacer la pirateria imposible.
Si el nivel gratuito es lo suficientemente generoso como para que la gente realmente lo use, algunos de ellos se convertiran en clientes de pago. Esa es la apuesta. Y hacer sh0 gratuito para desarrolladores individuales mientras se cobra a los equipos por funcionalidades de equipo es, creemos, la apuesta correcta para software de infraestructura auto-alojado.
Esta es la Parte 34 de la serie "Como construimos sh0.dev". A continuacion: el gran final -- 14 dias, 105 sesiones, 1 CTO de IA. La historia completa de la construccion de sh0.dev.