De cero a un club nuevo funcionando, con su app compilada

Una sola ruta, con cada dato explicado: qué es, de dónde sale y cómo comprobar que quedó bien. Elige tu escenario en la fase 1 (tipo de servidor, proveedor de correo, tiendas) y la guía muestra solo los pasos que te tocan. Los datos que escribas rellenan todos los comandos y archivos.

Visión general

Un club es una instalación independiente. Lo que cambia entre clubes vive en dos carpetas que no se mezclan con el código.

flowchart LR
  A["deploy/clients/slug · client.json + secrets.json"] --> B["tool/deploy.bat build"]
  A --> C["tool/deploy.bat backend-env"]
  B --> D["dist/builds · APK · AAB · web · latest.json"]
  D -->|"git push"| E["Bitbucket · repo de builds"]
  E -->|"pipeline rsync"| F["Servidor de actualizaciones"]
  F --> G["Teléfonos Android leen latest.json"]
  F --> H["Web / PWA"]
  C --> I["backend.env · .env del servidor"]
  
BackendLaravel 13 con PHP 8.4 y MySQL: API, correos, push, recordatorios.
AppFlutter: Android (APK/AAB), iOS (IPA) y web/PWA.
FirebaseUn proyecto por club para push en los tres dispositivos.
UpdatesCarpeta estática por HTTPS con latest.json y los APK.
Qué es automático y qué no. Android no deja actualizar en silencio un APK instalado fuera de Google Play: la app detecta la versión nueva, la descarga, verifica su huella SHA-256 y abre el instalador, y la persona confirma con un toque. La web/PWA se actualiza sola. Si publicas el .aab en Google Play, Play actualiza sin intervención.
Cuánto tarda. Con el servidor y los dominios listos, un club nuevo son unas 3 a 5 horas. Lo más lento es esperar a que el DNS propague (minutos a unas horas) y las revisiones de las tiendas, si las usas (Google Play y Apple tardan días).

Tu escenario y los datos del club

Primero eliges cómo vas a montar este club; después escribes los datos que ya tienes. Los demás (Firebase, claves) los pegas en sus fases, cuando existan.

Escenario

La web/PWA siempre se incluye. Puedes marcar APK y Google Play a la vez.

Identidad del club

Servidor y correo · déjalos vacíos para usar el valor sugerido (aparece en gris)

API ·
Web ·
Actualizaciones ·
Manifiesto que lee la app ·
Prefijo de variables Bitbucket ·

Datos del club definidos

Confirma que el slug, el ID de la app y el dominio son los definitivos. Cambiar el ID de la app o la firma después hace que los teléfonos ya instalados no puedan actualizarse.

Preparar tu equipo

Se hace una vez por computador. Después de instalar cada herramienta, el comando de la columna derecha debe responder sin error.

Instalar las herramientas

HerramientaDónde se consigueComprobación
Gitgit-scm.com/downloadsgit --version
Flutter (Dart 3.13 o superior)docs.flutter.dev/get-started/install. FVM es opcional: el script lo usa si lo encuentra y, si no, usa flutter.flutter doctor
Android Studio (trae JDK y Android SDK)developer.android.com/studio. En el primer arranque instala el SDK; luego acepta las licencias.flutter doctor --android-licenses
Xcode (solo Mac, solo si usarás iPhone)App Store del Macxcodebuild -version
PHP 8.4 y Composer 2En Windows, Laragon (laragon.org) trae PHP; selecciona 8.4 en su menú. Composer: getcomposer.orgphp -v y composer -V
Resultado esperado. flutter doctor debe mostrar visto bueno en Flutter y Android toolchain. Un aviso amarillo de Visual Studio o Chrome no afecta a este proyecto.

Preparar el proyecto Flutter

En la carpeta del proyecto (donde está pubspec.yaml):

PowerShell o CMD
flutter pub get
flutter gen-l10n
flutter analyze

Si usas FVM, antepón fvm a cada comando. flutter analyze puede mostrar avisos de estilo (nivel info); solo importan los que digan error.

Comprobar que el script de builds responde

PowerShell o CMD
tool\deploy.bat help
tool\deploy.bat list

El primero muestra los comandos disponibles; el segundo lista los clubes que ya existen en deploy/clients/.

Preparar tu acceso SSH a Bitbucket

Para subir código y builds desde tu PC sin escribir contraseña:

PowerShell o CMD
ssh-keygen -t ed25519 -C "tu-correo@ejemplo.com"
type %USERPROFILE%\.ssh\id_ed25519.pub

Pulsa Enter en todas las preguntas para aceptar la ruta y dejar la frase vacía. Copia la línea que imprime el segundo comando (en PowerShell es Get-Content $env:USERPROFILE\.ssh\id_ed25519.pub). En Bitbucket: tu foto de perfil · Personal settings · SSH keys · Add key, y pégala. Comprueba con ssh -T git@bitbucket.org: debe saludarte por tu nombre.

El servidor

Aquí vive la API, la base de datos y la carpeta de actualizaciones. Los pasos cambian según el escenario que elegiste en la fase 1.

Contratar el VPS

Cualquier proveedor sirve (Hetzner, DigitalOcean, Vultr, Contabo, Hostinger VPS…). Pide: Ubuntu 24.04 LTS, mínimo 2 GB de RAM y 40 GB de disco, y añade tu clave SSH al crearlo si te lo ofrece.

Dónde sale la IPEl panel del proveedor, en la ficha del servidor (IPv4 pública). La necesitas para el DNS en la fase siguiente.

Entrar al servidor y instalar todo

Desde tu PC: ssh root@LA_IP. Luego pega este bloque completo. Instala PHP 8.4, nginx, MySQL, Composer y certbot (HTTPS), activa el cortafuegos y crea el usuario de despliegue.

Servidor, como root
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get update && apt-get -y upgrade
apt-get -y install software-properties-common curl unzip git rsync ufw nginx mysql-server certbot python3-certbot-nginx supervisor
add-apt-repository -y ppa:ondrej/php
apt-get update
apt-get -y install php8.4-fpm php8.4-cli php8.4-mysql php8.4-mbstring php8.4-xml php8.4-curl php8.4-zip php8.4-gd php8.4-bcmath php8.4-intl
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# cortafuegos: solo SSH y web
ufw allow OpenSSH && ufw allow 'Nginx Full' && ufw --force enable

# usuario de despliegue, dueño del código; PHP corre con ese usuario
adduser --disabled-password --gecos "" {{sshUser}}
sed -i 's/^user = www-data/user = {{sshUser}}/; s/^group = www-data/group = {{sshUser}}/' /etc/php/8.4/fpm/pool.d/www.conf

# subidas grandes (telemetrías)
sed -i 's/^upload_max_filesize.*/upload_max_filesize = 150M/; s/^post_max_size.*/post_max_size = 160M/' /etc/php/8.4/fpm/php.ini
systemctl restart php8.4-fpm

# carpetas
mkdir -p {{appPath}} {{updRoot}}
chown -R {{sshUser}}:{{sshUser}} /var/www/{{slug}} {{updRoot}}
chmod 755 /var/www {{updRoot}}
Comprobación. php -v debe decir 8.4, y systemctl status nginx mysql php8.4-fpm debe mostrar los tres como active (running).

Si tu carpeta del backend o la de actualizaciones (fase 1) están fuera de , ajusta la línea de chown.

Crear la base de datos

Primero genera una clave larga para el usuario de la base de datos y guárdala en un gestor de contraseñas; irá en secrets.json como DB_PASSWORD.

Servidor, como root
openssl rand -base64 24        # copia la salida: es DB_PASSWORD
mysql

Dentro de mysql> (cambia la clave por la que generaste; evita comillas y barras en ella):

Consola de MySQL
CREATE DATABASE {{dbName}} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER '{{dbUser}}'@'localhost' IDENTIFIED BY 'LA_CLAVE_GENERADA';
GRANT ALL PRIVILEGES ON {{dbName}}.* TO '{{dbUser}}'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Recordatorios: el planificador

Los recordatorios de eventos y el resumen diario salen de un cron que corre cada minuto con el usuario de despliegue:

Servidor, como root
(crontab -u {{sshUser}} -l 2>/dev/null; echo '* * * * * cd {{appPath}} && php artisan schedule:run >> /dev/null 2>&1') | crontab -u {{sshUser}} -
crontab -u {{sshUser}} -l

Empieza a funcionar cuando se despliegue el backend; antes no hace nada.

Antes de empezar. Este camino exige que tu hosting ofrezca PHP 8.4 (cPanel · MultiPHP Manager), acceso SSH y rsync en el servidor. Pregúntaselo al soporte si no lo ves. Sin SSH no funcionan los pipelines de Bitbucket y tendrás que subir cada versión como .zip desde el Administrador de archivos.

Crear los tres subdominios

cPanel · Dominios (o Subdominios). Crea estos tres y, en cada uno, la raíz del documento exacta:

SubdominioRaíz del documento

La IP del servidor aparece en cPanel, en la columna derecha de la pantalla de inicio (Dirección IP compartida).

Elegir PHP 8.4 para el subdominio de la API

cPanel · MultiPHP Manager: marca el subdominio y asigna PHP 8.4. En MultiPHP INI Editor sube upload_max_filesize a 150M y post_max_size a 160M. Comprueba en Select PHP Version (extensiones) que estén activas: mbstring, pdo_mysql, curl, fileinfo, zip, gd, bcmath, intl y xml.

Crear la base de datos

cPanel · Bases de datos MySQL · Asistente. cPanel antepone tu usuario a los nombres: si tu usuario es cuenta y escribes kynetic, el nombre real es cuenta_kynetic. Ese nombre completo es el que debes poner en los campos Nombre de la base de datos y Usuario de la base de datos de la fase 1. Marca Todos los privilegios y guarda la contraseña que generes: irá en secrets.json como DB_PASSWORD.

Activar SSH y comprobar rsync

cPanel · Acceso SSH (a veces hay que pedir que lo habiliten). Desde tu PC: ssh usuario@dominio (puerto 22, o el que indique el panel). Dentro: which rsync composer php debe mostrar rutas. Si php -v no dice 8.4, el pipeline del backend no podrá correr artisan con esa versión: pide a soporte la ruta del PHP 8.4 y avísame para adaptar el pipeline.

Planificador (cron)

cPanel · Trabajos de cron · intervalo «Una vez por minuto» y comando:

Comando del cron
cd {{appPath}} && php artisan schedule:run >> /dev/null 2>&1

Muchos hostings limitan la frecuencia a cada 5 minutos: si es tu caso, los recordatorios llegan con ese retraso.

Antes de empezar. Necesitas un plan de Hostinger con acceso SSH (los más básicos no lo traen: mira hPanel · Avanzado · Acceso SSH) y PHP 8.4. Sin SSH no funcionan los pipelines de Bitbucket y habría que subir cada versión a mano. Los nombres de los menús de hPanel cambian con el tiempo; si no encuentras uno, usa el buscador de hPanel.

Crear los tres subdominios

hPanel · Dominios · Subdominios. Crea estos tres y, en cada uno, elige la carpeta personalizada exacta (si el formulario no la ofrece, créalo y luego edita su carpeta):

SubdominioCarpeta

Las rutas son relativas a tu carpeta personal; en hPanel puedes escribirlas como . Si el dominio está en Hostinger, los registros DNS de los subdominios se crean solos.

PHP 8.4 y límites de subida

hPanel · Avanzado · Configuración de PHP: elige PHP 8.4. En la pestaña Opciones de PHP sube upload_max_filesize a 150M y post_max_size a 160M. En Extensiones comprueba: mbstring, pdo_mysql, curl, fileinfo, zip, gd, bcmath, intl y xml.

Funciones desactivadas. Hostinger desactiva proc_open y exec en PHP. No las habilites: el pipeline ya está preparado para trabajar sin ellas (instala con --no-scripts y el enlace de storage se crea a mano).

Crear la base de datos

hPanel · Bases de datos · Bases de datos MySQL: escribe un nombre, un usuario y una contraseña. Hostinger antepone tu prefijo (u123456789_) a los dos: los nombres completos son los que van en «Nombre de la base de datos» y «Usuario de la base de datos» de la fase 1. Usa una contraseña larga sin espacios, comillas ni #; irá en secrets.json como DB_PASSWORD.

Activar SSH y probar la conexión

hPanel · Avanzado · Acceso SSH: actívalo. La pantalla muestra la IP, el puerto (65002) y el usuario. Pruébalo desde tu PC:

PowerShell o CMD
ssh -p {{sshPort}} {{sshUser}}@{{sshHost}}

Dentro, comprueba lo que necesita el despliegue:

En el servidor
php -v                 # debe decir 8.4
which rsync composer   # deben mostrar una ruta

Si php -v no dice 8.4, el despliegue puede fallar en artisan: pide a soporte la ruta del PHP 8.4 de línea de comandos.

Planificador (cron)

hPanel · Avanzado · Trabajos Cron: tipo personalizado, frecuencia cada minuto, y este comando:

Comando del cron
cd {{appPath}} && php artisan schedule:run >> /dev/null 2>&1

Empieza a servir cuando el backend esté desplegado.

El código del backend va fuera de la carpeta pública

Todo lo que está dentro de public_html se puede pedir por web. Si dejas ahí el backend completo, el .env (con tus claves) y la carpeta storage quedan a un error de configuración de ser públicos. Por eso se separan:

QuéDónde
Backend completo (artisan, .env, storage)
Raíz del documento del subdominio de la API

La carpeta del backend no debe colgar de public_html. Si ya creaste el subdominio apuntando a otra carpeta, edítalo (en Hostinger: hPanel · Dominios · Subdominios) y cámbiale la carpeta; no hace falta borrarlo.

Comprobación. Cuando el backend ya esté subido, abre en el navegador: debe dar 403 o 404, nunca mostrar texto. Si muestra algo, detén todo y cambia las claves: estuvieron expuestas.

Archivos .htaccess (Apache o LiteSpeed)

Tu hosting usa Apache o LiteSpeed, no nginx. Crea estos dos archivos con el Administrador de archivos (o por SSH). El primero va en (la web de la app); crea la carpeta web si aún no existe:

.htaccess de la web
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
<FilesMatch "^(index\.html|flutter_service_worker\.js|firebase-messaging-sw\.js)$">
  Header set Cache-Control "no-cache"
</FilesMatch>

El segundo va en (manifiesto y APK):

.htaccess de actualizaciones
Options -Indexes
AddType application/vnd.android.package-archive .apk
<FilesMatch "\.json$">
  Header set Cache-Control "no-cache"
</FilesMatch>

El pipeline de builds no los borra: su rsync excluye los .htaccess. Dentro de Laravel ya trae el suyo.

Modo prueba. Con Laragon puedes ver todo funcionando en tu PC, pero el servidor de actualizaciones, los pipelines y las notificaciones en teléfonos reales necesitan un servidor con HTTPS. Estos pasos sirven para probar; cuando quieras entregar el club, vuelve a la fase 1 y elige VPS o cPanel.

Backend con Laragon

  1. Coloca el backend en (Laragon lo expone como http://carpeta.test).
  2. Con Laragon encendido, abre HeidiSQL y crea la base (utf8mb4_unicode_ci). Usuario root sin contraseña.
  3. En la carpeta del backend: composer install, copy .env.example .env, php artisan key:generate.
  4. En .env pon APP_URL, DB_DATABASE y MAIL_MAILER=log (los correos quedan en storage/logs/laravel.log).
  5. php artisan migrate --seed y, en otra terminal, php artisan schedule:work.

Para el celular: el emulador de Android ve tu PC como 10.0.2.2; un teléfono real usa la IP de tu PC (ipconfig) y debe estar en la misma red Wi-Fi. Usa env/dev_android.json con flutter run.

Dominio, DNS y HTTPS

Tres nombres bajo el dominio del club apuntan a tu servidor. Sin HTTPS, Android rechaza la descarga del APK y el navegador bloquea el push.

Apuntar los nombres al servidor

En el panel del lugar donde compraste el dominio (o de Cloudflare, si lo usas) abre la sección DNS y crea tres registros:

TipoNombreValor
AapiLa IP del servidor
AappLa IP del servidor
AupdatesLa IP del servidor

Si usas Cloudflare, deja las nubes en «Solo DNS» (gris) hasta tener el HTTPS funcionando. Para comprobar la propagación:

PowerShell o CMD
nslookup {{apiHost}}
nslookup {{webHost}}
nslookup {{updHost}}
Comprobación. Los tres deben responder con la IP del servidor. Puede tardar de minutos a unas horas.
Dominio en Hostinger. Si el dominio está en Hostinger, no hace falta crear estos registros: se crean solos al crear los subdominios. Solo comprueba con nslookup.

Configurar los tres sitios en nginx

En el servidor, como root, pega todo el bloque:

cat > /etc/nginx/sites-available/{{slug}} <<'EOF'
# API Laravel
server {
    listen 80;
    server_name {{apiHost}};
    root {{appPath}}/public;
    index index.php;
    client_max_body_size 150M;
    location / { try_files $uri $uri/ /index.php?$query_string; }
    location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.4-fpm.sock; }
    location ~ /\.(?!well-known) { deny all; }
}

# Manifiesto y APK
server {
    listen 80;
    server_name {{updHost}};
    root {{updRoot}};
    autoindex off;
    index index.html;
    location / { try_files $uri $uri/ =404; add_header Cache-Control "no-cache"; }
    location ~* \.apk$ { default_type application/vnd.android.package-archive; add_header Cache-Control "public, max-age=3600"; }
}

# Web / PWA
server {
    listen 80;
    server_name {{webHost}};
    root {{updRoot}}/{{slug}}/web;
    location / { try_files $uri $uri/ /index.html; }
    location = /index.html                { add_header Cache-Control "no-cache"; }
    location = /flutter_service_worker.js { add_header Cache-Control "no-cache"; }
    location = /firebase-messaging-sw.js  { add_header Cache-Control "no-cache"; }
}
EOF
ln -sf /etc/nginx/sites-available/{{slug}} /etc/nginx/sites-enabled/{{slug}}
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx

Antes de continuar, nginx -t debe decir syntax is ok. Aunque la carpeta de la web aún no exista, nginx arranca; se crea con la primera publicación.

Emitir los certificados HTTPS

Servidor, como root
certbot --nginx --redirect --agree-tos -n -m TU_CORREO@ejemplo.com -d {{apiHost}} -d {{webHost}} -d {{updHost}}

Cambia TU_CORREO por un correo tuyo (Let's Encrypt avisa ahí antes de que venza un certificado). La renovación queda programada sola.

Comprobación. Desde tu PC: debe responder (un 404 es normal: aún no hay archivos). Si dice «certificate», el DNS todavía no propagó: espera y repite.

Activar el HTTPS

hPanel · Seguridad · SSL: Hostinger instala un certificado gratuito para el dominio y los subdominios; puede tardar unos minutos desde que existe el subdominio. Cuando cada uno abra con candado, comprueba sin avisos. Si no hay certificado, usa Instalar SSL en esa pantalla.

Activar el HTTPS

cPanel · SSL/TLS Status: selecciona los tres subdominios y pulsa Run AutoSSL. Cuando cada uno muestre un candado, abre en el navegador sin avisos de seguridad.

Correo

Un solo servidor SMTP y una sola contraseña; dos direcciones de remitente: para todos los avisos y para contratos y agenda de gestión humana. Si estás probando en local, salta esta fase (usa MAIL_MAILER=log).

Dos direcciones, una sola contraseña. El backend inicia sesión en el SMTP siempre con el buzón de avisos (MAIL_USERNAME y MAIL_PASSWORD). La dirección de gestión humana solo cambia el remitente que ven las personas. Por eso no necesitas su contraseña: crea esa dirección como alias o reenvío hacia un buzón real. Si tu proveedor rechaza enviar con un remitente distinto del usuario autenticado, deja HR_MAIL_ADDRESS vacío y gestión humana saldrá también desde el buzón de avisos.

Google Workspace o Gmail

  1. Crea la cuenta en el Admin de Workspace (admin.google.com · Directorio · Usuarios). Para gestión humana crea un alias en esa misma cuenta o un segundo usuario.
  2. Entra a esa cuenta y activa la verificación en dos pasos (myaccount.google.com · Seguridad).
  3. En la misma página busca Contraseñas de aplicaciones, crea una llamada «Kynetic» y copia las 16 letras (sin espacios). Esa es MAIL_PASSWORD.
  4. Para poder enviar como el correo de gestión humana: en el Gmail de la cuenta · Configuración · Cuentas e importación · Enviar como, añade la dirección.
DatoValor
MAIL_HOST
MAIL_PORT
MAIL_USERNAME
MAIL_PASSWORDLa contraseña de aplicación (va en secrets.json)
Límites. Gmail personal permite unos 500 envíos al día y Workspace unos 2.000. Para un club pequeño alcanza.

Zoho Mail

  1. Crea la cuenta en zoho.com/mail con tu dominio y verifica el dominio con el registro que te pida.
  2. Crea el buzón y otro (o un alias) para .
  3. En el buzón: Configuración · Seguridad · Contraseñas específicas de aplicación, genera una para «Kynetic». Esa es MAIL_PASSWORD.
  4. Confirma el servidor en Zoho · Configuración · Mail · Acceso IMAP/SMTP. Zoho usa smtp.zoho.com (cuentas .com) o smtp.zoho.eu (cuentas europeas); la región de tu cuenta decide. Si es la segunda, escríbela en el campo Servidor SMTP de la fase 1.
DatoValor
MAIL_HOST
MAIL_PORT
MAIL_USERNAME
Plan gratuito. El SMTP externo puede estar restringido en el plan gratuito; si te rechaza la conexión, revisa los planes de Zoho.

Correo del hosting (cPanel)

  1. cPanel · Cuentas de correo · Crear: y . Guarda sus contraseñas.
  2. En esa lista pulsa Conectar dispositivos junto al buzón de avisos. Ahí aparecen los datos exactos: servidor saliente (normalmente ), puerto 465 con SSL o 587.
DatoValor
MAIL_HOST
MAIL_PORT
MAIL_USERNAME
MAIL_PASSWORDLa contraseña del buzón (va en secrets.json)
Entrega. Los correos desde un hosting compartido caen más en spam. Si pasa, usa Brevo como SMTP.
Hostinger. hPanel · Correos electrónicos · Cuentas de correo · crear la cuenta. Hostinger no tiene contraseñas de aplicación: se usa la contraseña normal del buzón (larga, sin espacios, comillas ni #). Datos: servidor smtp.hostinger.com, puerto 465 (SSL) y usuario igual a la dirección completa. Para SPF y DKIM, si el DNS está en Hostinger, activa la autenticación en la misma pantalla de correos.

Brevo u otro servicio SMTP

  1. Crea la cuenta en brevo.com (plan gratuito: unos 300 correos al día).
  2. Añade y autentica tu dominio (Senders, Domains & Dedicated IPs · Domains): Brevo te da los registros DKIM y de verificación para el DNS.
  3. Añade y como remitentes verificados.
  4. SMTP & API · pestaña SMTP: copia el servidor, el puerto y el inicio de sesión, y genera una clave SMTP. En Brevo el usuario SMTP no es la dirección de correo: es el inicio de sesión que muestra ese panel.
DatoValor
MAIL_HOST
MAIL_PORT
MAIL_USERNAMEEl inicio de sesión SMTP de Brevo. Escríbelo en backend_env de client.json (fase de archivos)
MAIL_PASSWORDLa clave SMTP (va en secrets.json)

Con otros servicios (Mailgun, Amazon SES, Resend) el procedimiento es el mismo: verificar dominio, verificar remitentes, copiar servidor, puerto, usuario y clave SMTP.

Registrar SPF, DKIM y DMARC en el DNS

Sin estos tres registros, Gmail y Outlook mandan los avisos a spam. Se crean como registros TXT en el mismo panel DNS de la fase anterior.

RegistroNombreValor
SPF@Un solo registro TXT que empiece con v=spf1. El proveedor te da el valor exacto (Google: v=spf1 include:_spf.google.com ~all). Si ya hay uno, no crees otro: añade el include: al existente.
DKIMLo indica el proveedor (Google: google._domainkey)Una clave larga que genera el proveedor. Google: Admin · Aplicaciones · Gmail · Autenticar correo. Zoho: Panel de administración · Dominios · DKIM. cPanel: Email Deliverability.
DMARC_dmarc
Cómo comprobarloEnvía un correo de prueba a la dirección que te da mail-tester.com y mira su puntuación (busca 9/10 o más). Los tres registros deben aparecer en verde.

Firebase y notificaciones push

Un proyecto de Firebase atiende Android, iOS y web. Aquí obtienes hasta 11 datos: pégalos en los campos de abajo y se rellenan en client.json automáticamente. Todos son públicos salvo el archivo de cuenta de servicio.

Crear el proyecto

  1. Entra a console.firebase.google.com con una cuenta de Google que el club conserve (no la personal de un miembro).
  2. Crear un proyecto, con el nombre del club. Google Analytics es opcional; puedes desactivarlo.
  3. Al terminar, pulsa el engranaje · Configuración del proyecto · pestaña General.
Sale de aquíEn esa pantalla, arriba: ID del proyecto (ejemplo nexora-hub-4821; Google puede añadir números). Es tu «ID del proyecto».

Agregar la app Android

En General · Tus apps · icono de Android. Escribe el nombre de paquete exactamente igual. Puedes saltar la descarga de google-services.json: esta app no lo usa, los valores se pasan al compilar.

Cuando exista, entra a la tarjeta de la app y a Configuración del SDK · Config. De ahí copia dos valores a los campos de abajo:

  • ID de la aplicación: algo como 1:123456789012:android:0a1b2c3d4e5f
  • Clave de API (apiKey, empieza con AIza)

Agregar la app iOS

Tus apps · icono de Apple. ID del paquete: . De la tarjeta de la app copia el ID de la aplicación (1:…:ios:…) y la clave de API. Si descargas GoogleService-Info.plist, esos valores son GOOGLE_APP_ID y API_KEY.

Agregar la app Web

Tus apps · icono web </>. Apodo cualquiera; no marques Hosting. Firebase muestra un bloque firebaseConfig: de ahí salen apiKey y appId de la web, y también messagingSenderId y projectId, que ya tienes.

ID del remitente y clave VAPID

Configuración del proyecto · pestaña Cloud Messaging:

  • ID de remitente (Sender ID): número de unos 12 dígitos. «API de Firebase Cloud Messaging (V1)» debe estar Habilitada; si no, actívala con los tres puntos · Administrar API.
  • Configuración web · Certificados push web · Generar par de claves. Copia la clave pública, una cadena larga que empieza por B. Es la clave VAPID.

Pega aquí los valores

Estas claves de API de Firebase son públicas por diseño (van dentro de la app y de la web). Lo que nunca se comparte es la cuenta de servicio del paso siguiente.

Descargar la cuenta de servicio (la única clave secreta)

Configuración del proyecto · Cuentas de servicio · Generar nueva clave privada. Descarga un .json. Es lo que permite al backend enviar push; guárdalo fuera de git. Más adelante lo subes al servidor en .

Clave APNs para que lleguen avisos en iPhone

  1. En developer.apple.com/account (programa de pago, 99 USD al año) · Certificates, Identifiers & Profiles · Keys · «+». Marca Apple Push Notifications service (APNs) y descarga el .p8 (solo se descarga una vez).
  2. Anota el Key ID (10 caracteres) y tu Team ID (en la cuenta, sección Membership).
  3. En Firebase · Configuración · Cloud Messaging · Configuración de la app de Apple: sube el .p8 con el Key ID y el Team ID.
iPhone queda fuera. No marcaste iPhone en la fase 1, así que no necesitas app iOS ni clave APNs. Las personas con iPhone pueden usar la web instalada como PWA (iOS 16.4 o superior); los avisos push en web exigen que la instalen en la pantalla de inicio.

Firma de Android

Cada club tiene su propia clave. Android solo actualiza una app si la firma es idéntica a la instalada, y no hay forma de recuperar una clave perdida.

Generar el keystore

keytool viene con el JDK. Primero averigua dónde está en tu equipo: la ruta cambia según cómo instalaste Android Studio y no siempre es C:\Program Files\Android\Android Studio.

PowerShell
Get-Command keytool -ErrorAction SilentlyContinue
# si no muestra nada, busca en las ubicaciones habituales:
Get-ChildItem "C:\Program Files\Android","C:\Program Files\Java","$env:LOCALAPPDATA\Programs","$env:USERPROFILE\.jdks","C:\Program Files\Eclipse Adoptium" -Recurse -Filter keytool.exe -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName

Otra forma: flutter doctor -v muestra «Java binary at:»; keytool.exe está en esa misma carpeta bin. Si no hay ningún JDK instalado: winget install EclipseAdoptium.Temurin.17.JDK, cierra y abre PowerShell.

Con la ruta en la mano, ejecuta cada línea por separado (cambia RUTA_DE_KEYTOOL por la que encontraste):

PowerShell
mkdir C:\claves
& "RUTA_DE_KEYTOOL" -genkey -v -keystore C:\claves\{{slug}}.jks -keyalg RSA -keysize 2048 -validity 10000 -alias {{slug}}

Si el primer comando mostró que keytool ya está en el PATH, escribe simplemente keytool -genkey -v … sin ruta ni &. En CMD tampoco se usa el & inicial. Nunca juntes las dos líneas con &: PowerShell lo rechaza. Te hará preguntas:

  • Contraseña del almacén: mínimo 6 caracteres. Anótala.
  • Nombre y apellido, unidad, organización, ciudad, estado, país: pon los del club (el país como código de 2 letras, ej. CO). No afectan al funcionamiento.
  • ¿Es correcto?: escribe si o yes.
  • Contraseña de la clave: pulsa Enter para usar la misma que el almacén (más simple).
Respáldalo hoy. Guarda el .jks y las contraseñas en un gestor de contraseñas y en una copia fuera de tu PC. Si se pierden, la app de este club no podrá actualizarse nunca más y habrá que pedir a todos que la reinstalen.

Crear su key.properties

Dentro de (la carpeta la crea init-client en la fase de archivos; puedes crearla a mano antes) crea este archivo, que git ignora:

storePassword=LA_CONTRASEÑA_DEL_ALMACÉN
keyPassword=LA_CONTRASEÑA_DE_LA_CLAVE
keyAlias={{slug}}
storeFile=C:/claves/{{slug}}.jks

keyAlias debe ser el valor que pusiste en -alias. storeFile lleva ruta absoluta con barras normales (/), aunque estés en Windows.

Si publicas en Google Play

Play usa Play App Signing: la clave que acabas de crear actúa como clave de subida y Google guarda la definitiva. Aun así, conserva el .jks; la necesitas para cada versión. Si además instalas APK directos, los teléfonos que instalaron por Play y los que instalaron por APK tienen firmas distintas y no pueden actualizarse entre sí: decide por cada persona cuál canal usará.

Archivos del cliente

Es la única configuración por club. De aquí salen el .env del backend, los valores de la app y el nombre, el ID y la firma de Android.

Crear la carpeta del cliente

Raíz del proyecto Flutter
tool\deploy.bat init-client {{slug}} --name "{{name}}"

Crea con client.json y secrets.json a partir de la plantilla. En el siguiente paso reemplazas su contenido.

Escribir client.json

Este es el archivo completo con todo lo que escribiste. Los valores que todavía faltan aparecen como PEGA_…; completa los de Firebase en su fase y esta vista se actualiza sola.

    {
      "slug": "{{slug}}",
      "name": "{{name}}",
      "environment": "prod",
      "android": {
        "applicationId": "{{appId}}",
        "keyProperties": "deploy/clients/{{slug}}/key.properties"
      },
      "ios": { "bundleId": "{{bundleId}}" },
      "urls": {
        "api": "{{api}}",
        "web": "{{web}}",
        "updates": "{{updatesJson}}"
      },
      "update": { "mandatory": false, "minSupportedBuild": 0 },
      "firebase": {
        "projectId": "{{fbProject}}",
        "senderId": "{{senderId}}",
        "android": { "apiKey": "{{fbAndroidKey}}", "appId": "{{fbAndroidApp}}" },
        "ios": { "apiKey": "{{fbIosKey}}", "appId": "{{fbIosApp}}" },
        "web": { "apiKey": "{{fbWebKey}}", "appId": "{{fbWebApp}}", "vapidKey": "{{vapid}}" }
      },
      "backendDir": "{{backendDirJson}}",
      "backend_env": {
        "DB_DATABASE": "{{dbName}}",
        "DB_USERNAME": "{{dbUser}}",
        "MAIL_MAILER": "smtp",
        "MAIL_HOST": "{{mailHost}}",
        "MAIL_PORT": "{{mailPort}}",
        "MAIL_SCHEME": "{{mailScheme}}",
        "MAIL_USERNAME": "{{mailUser}}",
        "MAIL_FROM_ADDRESS": "{{mailUser}}",
        "HR_MAIL_ADDRESS": "{{hrMail}}",
        "SESSION_DOMAIN": ".{{domain}}",
        "NOTIFICATIONS_TIMEZONE": "{{tz}}",
        "HR_DEFAULT_CURRENCY": "{{currency}}"{{s3Json}}
      }
    }
    Qué se deriva soloEl script calcula APP_URL, APP_WEB_URL, FRONTEND_URL, CORS_ALLOWED_ORIGINS, SANCTUM_STATEFUL_DOMAINS, FCM_* y las variables de producción a partir de urls y firebase. Cualquier variable de backend_env pisa a la derivada.
    Solo Google Play. Dejé urls.updates vacío: sin él la app no consulta actualizaciones propias y Play se encarga. La web y la PWA siguen necesitando un lugar donde publicarse (urls.web).
    Brevo. MAIL_USERNAME debe ser el inicio de sesión SMTP que muestra Brevo, no la dirección de correo. Cámbialo en este archivo.

    Generar APP_KEY y escribir secrets.json

    Todo lo secreto va aquí; git lo ignora. Primero genera la clave de cifrado de Laravel desde la carpeta del backend en tu PC:

    PowerShell o CMD
    cd "{{backendDir}}"
    php artisan key:generate --show

    Imprime una línea que empieza con base64:. Cópiala completa. Después edita el archivo:

    {
      "APP_KEY": "base64:PEGA_LA_LINEA_COMPLETA",
      "DB_PASSWORD": "la clave que generaste para MySQL",
      "MAIL_PASSWORD": "la contraseña de aplicación o SMTP",
      "GEMINI_API_KEY": ""
    }
    De dónde sale cada unaDB_PASSWORD: la que generaste al crear la base de datos. MAIL_PASSWORD: la de la fase de correo. GEMINI_API_KEY es opcional: entra a aistudio.google.com/apikey con una cuenta de Google y pulsa Create API key; sin ella desaparece el botón «Evaluar con IA».
    APP_KEY no se cambia. Con ella se cifran datos de la base. Respáldala junto con las demás contraseñas.

    Archivos en S3

    En la consola de AWS: crea un bucket (S3 · Create bucket, con el acceso público bloqueado), luego en IAM un usuario con permiso solo sobre ese bucket y genera sus claves de acceso. Añade al secrets.json AWS_ACCESS_KEY_ID y AWS_SECRET_ACCESS_KEY. En el client.json de arriba reemplaza PEGA_NOMBRE_DEL_BUCKET por el nombre del bucket y ajusta la región (la que aparece junto al bucket en S3).

    Bitbucket y pipelines

    Tres repositorios, tres pipelines, cada uno con una sola tarea. Esta fase deja listas las claves que permiten a Bitbucket entrar a tu servidor.

    Cuántas claves son. Cada repositorio (builds y backend) genera su propia clave SSH y las dos van al mismo servidor, una por línea. Si pegas la de un repositorio y lanzas el pipeline del otro, falla con Permission denied.
    RepositorioQué hace su pipelineCuándo corre
    App FlutterAnaliza, prueba y revisa imports de plataformaCada push
    BackendPruebas; y deploy-client sincroniza, migra y recarga cachésPruebas en cada push; el despliegue lo lanzas tú
    BuildsCopia por rsync el APK, latest.json y la web al servidor de actualizacionesCada push a main

    Crear los repositorios y subir el código

    En bitbucket.org · Create · Repository. Crea tres, vacíos (sin README): uno para la app, otro para el backend y otro para los builds (por ejemplo kynetic-builds). Para cada uno, el botón Clone · pestaña SSH da su dirección (git@bitbucket.org:espacio/repo.git).

    Sube el código que ya tienes (si todavía no tiene repositorio git):

    En la carpeta de la app y, aparte, en la del backend
    git init -b main
    git add .
    git commit -m "Primer commit"
    git remote add origin git@bitbucket.org:TU-ESPACIO/NOMBRE-DEL-REPO.git
    git push -u origin main

    Si la carpeta ya era un repositorio, solo hace falta el git remote add y el git push. El repositorio de builds no hace falta crearlo localmente: lo hace tool\deploy.bat la primera vez con --remote.

    Activar Pipelines y crear la clave SSH

    Hazlo en el repositorio de builds y repite igual en el del backend. En el repositorio · Repository settings:

    1. Pipelines · Settings · activa Enable Pipelines.
    2. Pipelines · SSH keys · Generate keys. Aparece una clave pública (una sola línea que empieza con ssh-rsa); cópiala completa.
    3. Autorízala en el servidor:

      hPanel · Avanzado · Acceso SSH: si tu plan ofrece agregar una clave SSH, pégala ahí. Si no, hazlo por terminal (siguiente bloque).

      Servidor, como el usuario de despliegue
      mkdir -p ~/.ssh && chmod 700 ~/.ssh
      echo 'PEGA_AQUI_LA_CLAVE_PUBLICA' >> ~/.ssh/authorized_keys
      chmod 600 ~/.ssh/authorized_keys
      cat ~/.ssh/authorized_keys    # cada clave debe ocupar UNA sola línea

      Para entrar al servidor desde tu PC: (en un VPS, entra primero como root y usa ). La clave del otro repositorio se añade en otra línea.

    4. Known hosts: no hace falta. Los pipelines aceptan la huella del servidor la primera vez que se conectan. El botón Fetch de Bitbucket solo prueba el puerto 22, así que con SSH en otro puerto (Hostinger usa el 65002) siempre dice «Unable to fetch fingerprints». Es normal: ignóralo.
    Comprobación. La verdadera prueba es lanzar el pipeline. Si ves Permission denied (publickey), la clave no está en authorized_keys (o es la del otro repositorio).

    Variables del repositorio de builds

    Repository settings · Pipelines · Repository variables. Agrega estas cuatro (sin marcar como secured; no son secretas). Los nombres no llevan el prefijo del club:

    VariableValor
    DEPLOY_USER
    DEPLOY_HOST
    DEPLOY_PORT
    DEPLOY_PATH
    DEPLOY_PATH se vacía. El pipeline deja esa carpeta idéntica al repositorio de builds y borra todo lo demás (menos los .htaccess). Que sea solo la carpeta de updates, nunca la del backend ni tu carpeta personal.
    Pipeline actualizado. El archivo bitbucket-pipelines.yml del repositorio de builds lo copia tool\deploy.bat en cada compilación. Si ves en el log el pipe: atlassian/rsync-deploy y un «port 22: Operation timed out», tu repositorio de builds tiene la versión vieja: compila otra vez o copia deploy\builds-repo\bitbucket-pipelines.yml a dist\builds y haz push.

    Variables del repositorio del backend

    VariableValor

    A diferencia del repositorio de builds, aquí los nombres sí llevan el slug en mayúsculas y con guion bajo en lugar de guion: así un mismo repositorio puede desplegar varios clubes (el club se elige al lanzar el pipeline). _APP_PATH es la carpeta del backend (la que contiene artisan), nunca la carpeta public. Usa la ruta absoluta.

    Backend en producción

    El .env se genera en tu PC y se sube una vez. Los secretos nunca pasan por Bitbucket. El orden importa: primero el .env, luego el código.

    Decidir quién será el súper administrador

    La primera cuenta con todos los permisos se crea desde database/seeders/SuperAdminSeeder.php. Ese archivo trae los datos de una cuenta (nombre, apodo, correo, cumpleaños, NIT) y una contraseña inicial. Antes del primer despliegue:

    1. Edita la constante SUPER_ADMIN con los datos de la persona que será dueña del club.
    2. Revisa la contraseña inicial (en run() con Hash::make('…'), o en SUPER_ADMIN_PASSWORD_HASH si tu versión usa un hash). Para generar un hash: php artisan tinker y echo bcrypt('LA_CONTRASEÑA');.
    3. Haz commit y push al repositorio del backend antes del primer despliegue.
    Cámbiala al entrar. Una contraseña escrita en el código queda en el historial de git. Úsala solo para el primer ingreso y cámbiala enseguida desde Ajustes.

    El seeder nunca crea un segundo súper admin ni cambia una contraseña existente. Las pruebas automáticas leen estos datos del propio seeder, así que no hace falta tocarlas.

    Generar el .env del club

    Raíz del proyecto Flutter
    tool\deploy.bat backend-env --client {{slug}} --backend "{{backendDir}}"

    Combina el .env.example del backend con client.json y secrets.json y lo deja en (ignorado por git). Ábrelo y busca líneas vacías que necesiten valor: deben tener contenido APP_KEY, DB_PASSWORD y MAIL_PASSWORD.

    Subir el .env y la cuenta de servicio de Firebase

    PowerShell o CMD en tu PC
    ssh -p {{sshPort}} {{sshUser}}@{{sshHost}} "mkdir -p {{appPath}}/storage/app"
    scp -P {{sshPort}} deploy\private\{{slug}}\backend.env {{sshUser}}@{{sshHost}}:{{appPath}}/.env
    ssh -p {{sshPort}} {{sshUser}}@{{sshHost}} "chmod 640 {{appPath}}/.env"
    scp -P {{sshPort}} RUTA\DEL\ARCHIVO-DE-FIREBASE.json {{sshUser}}@{{sshHost}}:{{appPath}}/storage/app/firebase-service-account.json

    La cuarta línea sube el JSON de cuenta de servicio de la fase de Firebase; ajusta la ruta de origen. Fíjate en la mayúscula: ssh usa -p y scp usa -P. El pipeline no toca .env ni storage/, así que estos archivos sobreviven a los despliegues.

    Primer despliegue del código

    Primero haz commit y push de todo el backend (incluidos los cambios del seeder). En el repositorio del backend: Pipelines · Run pipeline · rama main · pipeline Custom: deploy-client · variable . Corre las pruebas, sube el código al servidor, crea las carpetas de storage, instala dependencias (composer install --no-dev --no-scripts, luego package:discover), migra la base y arma las cachés.

    Comprobación. Los pasos «Pruebas» y «Desplegar» terminan en verde. Si falla, lee el último error del log: casi siempre es uno de los de la sección de problemas frecuentes (clave SSH, proc_open, base de datos o .env).
    Sin SSH. Si tu hosting no permite SSH, comprime el backend sin vendor, .env ni storage, súbelo con el Administrador de archivos a , crea a mano las carpetas storage/framework/{cache/data,sessions,views}, storage/logs y bootstrap/cache, y ejecuta composer install --no-dev y php artisan migrate --force desde la Terminal del hosting.

    Comandos de una sola vez: datos mínimos

    Conéctate con y ejecuta. Es lo mínimo para que el súper admin entre y cree miembros, ramas y todo lo demás desde la app: estados de usuario, roles, módulos y permisos, la cuenta del súper admin y el tema del club. El orden importa.

    Servidor, como el usuario de despliegue
    cd {{appPath}}
    php artisan db:seed --class=UserStatusSeeder --force
    php artisan db:seed --class=RolesSeeder --force
    php artisan db:seed --class=PermissionSeeder --force
    php artisan db:seed --class=SuperAdminSeeder --force
    php artisan db:seed --class=ThemeSeeder --force
    php artisan config:cache

    El último seeder imprime Super admin creado: …. Con eso ya puedes entrar y operar.

    Los cinco hacen falta. Sin PermissionSeeder la tabla de módulos y permisos queda vacía: el súper admin pasa las autorizaciones del servidor, pero la app arma sus menús con la lista de permisos que recibe al iniciar sesión, y llega vacía; el módulo Miembros no aparece. Sin ThemeSeeder no existe la fila del tema y Identidad responde 404. Si ya habías entrado, cierra sesión y vuelve a entrar para que se lea la lista nueva.
    Seeder que NO necesitasPara qué sirve
    DemoSeederDatos de ejemplo. No lo corras en producción.

    RolesSeeder también crea los roles admin, coach y player; si no los quieres, bórralos del archivo antes de desplegar. Cada seeder se puede repetir sin duplicar datos.

    Enlace de archivos públicos: php artisan storage:link.

    Enlace de archivos públicos. php artisan storage:link falla en hosting compartido (Call to undefined function exec()). Créalo a mano:
    Servidor
    cd {{appPath}}
    ln -s "$(pwd)/storage/app/public" public/storage
    ls -l public/storage
    Si dice que ya existe, bórralo con rm public/storage y repite. Si no sirves archivos desde storage/app/public, no hace falta.

    Comprobar la API

    Abre en el navegador . Debe devolver un JSON con colores. Si ves un error 500, mira el registro: .

    Cola de trabajos (solo si la usas)

    Los correos y el push no la necesitan. Solo hace falta un proceso de cola si en tu .env tienes RATING_AI_DISPATCH=queue (Evaluar con IA en segundo plano). En un VPS, Supervisor lo mantiene vivo: crea /etc/supervisor/conf.d/queue.conf con un programa que ejecute con user= el usuario de despliegue, y corre supervisorctl reread && supervisorctl update. Si dejas RATING_AI_DISPATCH en su valor por defecto, sáltate este paso.

    Servidor de actualizaciones

    Los sitios ya están configurados en la fase de dominio. Aquí compruebas que el servidor puede recibir lo que publica el pipeline y entiendes qué lee el teléfono.

    Comprobar que el servidor puede recibir archivos

    Conéctate como el usuario de despliegue y verifica tres cosas:

    Servidor, como el usuario de despliegue
    which rsync                         # debe mostrar una ruta
    ls -ld {{updRoot}}                   # el dueño debe ser este usuario
    touch {{updRoot}}/prueba && rm {{updRoot}}/prueba && echo "escritura ok"

    Qué lee el teléfono

    La app compilada para este club consulta . Estructura del archivo, que genera el script y no se edita a mano:

    android/latest.json
    {
      "version_name": "1.4.0",
      "version_code": 17,
      "apk_url": "nexora-1.4.0-b17.apk",
      "sha256": "9f2c…",
      "size": 48213377,
      "notes": "Corrige el calendario",
      "mandatory": false,
      "min_supported_code": 0
    }

    La app compara version_code con el número de compilación instalado (lo que va después del + en pubspec.yaml). Si es mayor, ofrece la actualización.

    Íconos de la app y página de descargas

    Una imagen por club genera el ícono de Android, de iPhone y de la PWA, el favicon del navegador y el ícono de la app de Windows. Y cada build deja una página con botones para descargar la app, que es la que compartes con la gente.

    Preparar la imagen

    • Un PNG o JPG cuadrado de 1024 × 1024 px o más. Si no es cuadrado se recorta desde el centro.
    • Deja un margen de alrededor del 10 %: los teléfonos redondean las esquinas.
    • iPhone no admite transparencia: los fondos transparentes se rellenan con el color de fondo del club (blanco si no defines otro, ver el paso de colores).
    • Opcionales: una versión simplificada para el favicon (se ve a 16 a 48 px: mejor un símbolo sin letras pequeñas), otra para Windows, y una maskable (el logo ocupa el 80 % central, para íconos redondos). Si no las das, se generan solas desde la principal.

    Constructor del comando

    PowerShell o CMD, raíz del proyecto Flutter

    Guardar los íconos del club

    La primera vez, descarga las dos dependencias nuevas del script:

    PowerShell o CMD
    flutter pub get

    Después ejecuta el comando del constructor. Copia tus imágenes a (icon.png, favicon.png, windows.png, maskable.png). Esa carpeta sí se sube a git: no tiene nada secreto. Para cambiar un ícono más adelante, repite el comando con la imagen nueva.

    Resultado esperado. El comando imprime «Guardado: …/icon.png». Si dice que no puede leer la imagen, usa un PNG o JPG normal.

    Qué se genera al compilar

    DóndeQué
    AndroidLos 5 tamaños del ícono del launcher (48 a 192 px).
    iPhoneLos 15 tamaños del set de ícono de la app, sin transparencia. Sirven para el IPA y también cuando se instala la PWA (ícono de pantalla de inicio).
    Web / PWAfavicon, Icon-192, Icon-512 y las dos versiones maskable.
    Windowsapp_icon.ico con 7 tamaños (16 a 256 px).

    Los íconos se aplican solo durante la compilación de ese club y al terminar el proyecto queda como estaba: no se ensucian los archivos de git ni se mezclan los clubes. Si un build se interrumpe, el siguiente restaura los originales antes de empezar.

    Nombre y colores del club (opcional)

    En puedes añadir un bloque branding. Todos los campos son opcionales:

    client.json
    "branding": {
      "accent": "#ff6a2b",
      "background": "#ffffff",
      "themeColor": "#120E0B",
      "shortName": "{{name}}"
    },
    CampoQué hace
    accentColor de los botones de la página de descargas.
    backgroundColor que rellena la transparencia del ícono de iPhone y el margen del ícono maskable. Blanco si no lo defines.
    themeColorColor de la barra del navegador y de la PWA instalada.
    exeNameNombre del archivo .exe de Windows, solo letras, números y guion bajo. Si no lo defines, se usa el slug.
    shortNameNombre corto bajo el ícono cuando instalan la web en el teléfono (de 12 caracteres hacia abajo se ve completo).

    El título de la pestaña, el nombre de la PWA y el nombre bajo el ícono de iPhone salen del name del club en cada build; ya no se queda el de la plantilla.

    La página de descargas

    Cada build genera y se publica sola con el pipeline. Es la dirección que compartes:

    Enlace para compartir
    {{updates}}/
    • Android: botón «Descargar APK» con la versión, el peso y cómo instalarlo.
    • iPhone y iPad: la guía para instalar la app web desde Safari (Compartir · Añadir a pantalla de inicio; iOS 16.4 o superior).
    • Windows: botón para descargar el .zip y cómo abrirlo (aparece cuando publicas un build de Windows).
    • Navegador: botón para abrir la versión web.
    • Código QR de la página, para que alguien la abra escaneándolo, y el aviso «Recomendado para tu dispositivo» según el teléfono o computador desde el que se abre.

    Al compilar solo la web, los botones de Android y Windows no desaparecen: la página lee los datos de los últimos builds publicados.

    Servidor. En un VPS con nginx, el sitio de actualizaciones debe tener index index.html; y try_files $uri $uri/ =404; (la fase de dominio ya lo trae). Sin eso, la dirección del club da 404 aunque el archivo exista.

    La app de Windows

    • Solo se compila en un computador con Windows, con Visual Studio 2022 y la carga de trabajo «Desarrollo para el escritorio con C++». flutter doctor debe marcar visto bueno en «Visual Studio».
    • Se compila con --targets windows (el constructor de la fase siguiente ya tiene la casilla). Si tienes Inno Setup instalado (winget install JRSoftware.InnoSetup), sale un instalador -setup.exe: se instala por usuario en %LOCALAPPDATA%\Programs sin pedir administrador, crea acceso en el menú Inicio y el escritorio, y se desinstala desde Configuración. Además se publica un .zip portátil. Sin Inno Setup solo sale el .zip.
    • Actualización automática: la app instalada lee windows/latest.json, descarga el instalador nuevo, comprueba el SHA-256 y lo ejecuta en silencio; la app se cierra y se vuelve a abrir actualizada. Para probarla, compila otra versión con un número de build mayor.
    • Ruta corta: mantén el proyecto en una ruta corta (por ejemplo C:\dev\kynetic); con rutas largas MSBuild falla al compilar plugins de Windows.
    • El instalador y el ejecutable no están firmados con certificado de editor: Windows mostrará «SmartScreen protegió su PC» y hay que pulsar Más información · Ejecutar de todas formas (la página de descargas lo explica).
    • El título de la ventana, las propiedades del archivo y el nombre del .exe salen del club al compilar: el título y las propiedades usan el name de client.json, y el ejecutable usa el slug con guiones bajos (por ejemplo con guiones bajos) o el branding.exeName que definas. Los archivos de Windows del proyecto se modifican solo durante la compilación y luego se restauran.
    Sin probar. Es la ruta que menos pude verificar: no pude compilar para Windows desde aquí ni sé si todos los paquetes de la app (por ejemplo los avisos push) funcionan en escritorio. Si flutter build windows falla, pega el error y lo resolvemos.

    Generar los builds

    Un comando por club. Antes del primero, repasa que todo lo necesario existe.

    Lista previa al primer build

    • sin ningún PEGA_… (la vista de la fase de archivos lo marca).
    • y el .jks en su sitio.
    • flutter pub get ejecutado al menos una vez.
    • urls.updates apuntando a un servidor con HTTPS que responda.
    • Si quieres los íconos del club, haber hecho la fase anterior (set-icons). Sin ellos se usan los íconos por defecto.
    • Un Mac con Xcode y la cuenta de Apple (ver la fase de tiendas) para el IPA.

    Constructor de comandos

    PowerShell o CMD, raíz del proyecto Flutter

    Primer build con publicación

    Marca «Primera vez» arriba para que el script cree dist/builds, copie el pipeline, haga git init y suba al repositorio de builds. La primera compilación de Android descarga Gradle y tarda varios minutos; las siguientes, menos.

    Resultado esperado. Termina con una lista de archivos generados y, con --push, un push a Bitbucket. En Bitbucket · Pipelines, el pipeline de builds corre y termina en verde.

    Qué sale de cada destino

    DestinoArchivoPara qué
    apkInstalación directa y actualizaciones desde la app. Va al servidor.
    aabGoogle Play. Queda local, no se sube a git ni al servidor.
    webPWA con el service worker de push ya rellenado con los datos del club.
    windowsApp de escritorio: la gente descomprime y ejecuta. Solo se compila en Windows.
    (siempre)Página de descargas del club: APK, guía de iPhone, Windows y código QR.
    ipaApp Store Connect y TestFlight. Solo desde un Mac con tool/deploy.sh.

    Comprobar la publicación en el servidor

    Abre en el navegador: debe mostrar el JSON con la versión y el build que acabas de compilar. Abre : debe cargar la app. Y abre : debe aparecer la página de descargas del club.

    Atajo. tool\publicar_todo.bat "Novedades" equivale a build --all --targets apk,aab,web --notes "Novedades" --push.
    Íconos. Hoy los íconos de la app son los mismos para todos los clubes. Si necesitas uno distinto por club, hay que ampliar el script para que copie el set de íconos desde deploy/clients/<slug>/.

    Google Play y App Store

    Solo aplica si marcaste Google Play o iPhone en la fase 1. Ambas tiendas revisan la app y pueden tardar días.

    Crear la cuenta y la ficha en Google Play

    1. play.google.com/console: cuenta de desarrollador (pago único de 25 USD). Verifica tu identidad; en cuentas personales nuevas, Google exige pruebas cerradas con testers antes de publicar a todos.
    2. Crear app: nombre, idioma, tipo «App», gratuita.
    3. Completa Contenido de la app: política de privacidad (una URL pública), declaración de datos y clasificación.

    Subir el AAB

    Compila con --targets aab y sube en Pruebas · Prueba interna · Crear versión. Añade los correos de tus testers y comparte el enlace de invitación. Cuando todo esté bien, promueve la versión a Producción.

    Cada versión nueva necesita un número de build mayor; el script ya lo hace. Si el club usa solo Play, deja urls.updates vacío para que la app no ofrezca su propio instalador.

    Preparar la cuenta de Apple

    1. developer.apple.com/programs: Apple Developer Program (99 USD al año; la verificación de organizaciones pide un número D-U-N-S).
    2. Certificates, Identifiers & Profiles · Identifiers · «+» · App IDs: crea uno con el Bundle ID y activa la capacidad Push Notifications.
    3. appstoreconnect.apple.com · Mis apps · «+» · Nueva app, con ese Bundle ID.

    Compilar y subir el IPA (en un Mac)

    1. En el Mac abre ios/Runner.xcworkspace con Xcode · Runner · Signing & Capabilities: marca Automatically manage signing, elige tu Team, comprueba el Bundle ID y añade las capacidades Push Notifications y Background Modes · Remote notifications.
    2. Compila con .
    3. Sube el IPA con la app Transporter (App Store) o desde Xcode · Window · Organizer. Aparece en App Store Connect · TestFlight tras unos minutos de procesamiento.
    Sin probar. El camino de iOS es el que menos he podido verificar. Si flutter build ipa falla por firma o perfiles, suele ser configuración de Xcode: pega el error y lo resolvemos.

    Instalar y verificar

    Antes de dárselo al equipo, comprueba cada camino con un dispositivo real.

    Primer ingreso y marca del club

    Abre e inicia sesión con la cuenta de súper administrador (la del seeder; ver la fase de backend). Cambia la contraseña en Ajustes. Luego abre Club · Identidad: sube el logo (PNG cuadrado, fondo transparente, 512 px o más) y guarda la paleta. De ahí salen los colores de la app, de los correos y el logo de las notificaciones.

    Instalar el primer APK

    Abre en el teléfono el enlace (el nombre exacto aparece en latest.json) o muéstrales un QR. Android pedirá permitir «instalar apps desconocidas» para el navegador. Esta primera instalación es manual para todas las personas.

    Probar la actualización de punta a punta

    1. Con la versión instalada, compila y publica otra con las opciones por defecto (sube el número de build).
    2. Abre la app en el teléfono. Debe aparecer la tarjeta «Hay una versión nueva» con tus notas (la consulta se repite como máximo cada 6 horas, salvo al abrir la app).
    3. Pulsa Actualizar, espera la descarga y confirma en el instalador de Android.

    Android la acepta solo con el mismo ID de app, la misma firma y un build mayor.

    Probar un aviso push en cada dispositivo

    Asígnale una tarea a otra persona. Debe llegar el aviso en un Android real, en un iPhone (PWA instalada con Compartir · Añadir a pantalla de inicio, iOS 16.4 o superior) y en Chrome de escritorio, y al tocarlo debe abrir /tareas. Cada persona activa los avisos desde la campana.

    Comprobar correos y recordatorios

    Servidor, carpeta del backend
    php artisan tinker
    >>> \App\Models\User::first()->notify(new \App\Notifications\AccountActivatedNotification);
    >>> exit
    php artisan notifications:daily
    php artisan notifications:event-reminders

    El correo debe llegar a la bandeja principal con el logo y los colores del club. Si no, revisa SPF, DKIM y DMARC (fase de correo) y el registro del backend.

    Abrir el club al equipo

    Crea una rama por juego, revisa los roles, genera invitaciones KYN-XXXX-XXXX y cada persona se registra con su código. Prueba también la subida de un archivo grande y activa la copia de seguridad de la base de datos y de storage/app.

    Operar el día a día

    Lo que haces después de la puesta en marcha.

    Publicar una versión nueva

    Compila con el constructor de la fase de builds. La app de cada teléfono la ofrece al abrirse o al volver al primer plano.

    Forzar una actualización

    --mandatory bloquea la pantalla hasta actualizar esa versión. --min-supported N obliga a todo build con número menor a N, útil cuando una versión vieja ya no funciona con el backend. En client.json el equivalente permanente es update.mandatory y update.minSupportedBuild.

    Retirar una versión mala

    Android no instala un build menor que el instalado. Corrige el problema y publica un build con número mayor. Para detener una publicación que aún no llegó a los teléfonos, revierte el commit en el repositorio de builds y el pipeline restaura el servidor.

    Desplegar cambios del backend

    Pipelines · Run pipeline · Custom: deploy-client con el slug del club. Corre las pruebas, sincroniza el código, composer install, migrate --force, cachés y queue:restart. Si cambias el .env, súbelo a mano y corre php artisan config:clear && php artisan config:cache.

    Rotar una contraseña o un secreto

    Edítalo en secrets.json, vuelve a correr backend-env, sube el nuevo .env y limpia la caché de configuración. No rotes APP_KEY ni la firma de Android.

    Añadir otro club

    Repite esta guía desde la fase 1 con otro slug, dominio, ID de app, Firebase y firma. Reutilizas los mismos repositorios de Bitbucket: solo añades las cuatro variables del backend (<SLUG>_SSH_*) y compilas con --client otro-slug.

    Si el repositorio de builds crece demasiado

    Cada APK queda en el historial de git aunque se borre de la carpeta (--keep conserva los últimos 3). Activa Git LFS (hay una línea comentada en el .gitattributes del repositorio de builds) o crea un repositorio nuevo cada año.

    Club terminado

    Todas las fases están cerradas. Guarda en un lugar seguro el .jks, sus contraseñas, secrets.json y backend.env.

    Referencia

    Cada variable, de dónde sale y quién la rellena.

    VariableArchivoQué es y de dónde saleLa rellena

    Problemas frecuentes