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"]
latest.json y los APK..aab en Google Play, Play actualiza sin intervención.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)
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
| Herramienta | Dónde se consigue | Comprobación |
|---|---|---|
| Git | git-scm.com/downloads | git --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 Mac | xcodebuild -version |
| PHP 8.4 y Composer 2 | En Windows, Laragon (laragon.org) trae PHP; selecciona 8.4 en su menú. Composer: getcomposer.org | php -v y composer -V |
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):
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
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:
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.
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.
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}}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.
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):
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:
(crontab -u {{sshUser}} -l 2>/dev/null; echo '* * * * * cd {{appPath}} && php artisan schedule:run >> /dev/null 2>&1') | crontab -u {{sshUser}} -
crontab -u {{sshUser}} -lEmpieza a funcionar cuando se despliegue el backend; antes no hace nada.
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:
| Subdominio | Raí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:
cd {{appPath}} && php artisan schedule:run >> /dev/null 2>&1Muchos hostings limitan la frecuencia a cada 5 minutos: si es tu caso, los recordatorios llegan con ese retraso.
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):
| Subdominio | Carpeta |
|---|---|
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.
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:
ssh -p {{sshPort}} {{sshUser}}@{{sshHost}}Dentro, comprueba lo que necesita el despliegue:
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:
cd {{appPath}} && php artisan schedule:run >> /dev/null 2>&1Empieza 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.
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:
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):
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.
Backend con Laragon
- Coloca el backend en
(Laragon lo expone comohttp://carpeta.test). - Con Laragon encendido, abre HeidiSQL y crea la base
(utf8mb4_unicode_ci). Usuariorootsin contraseña. - En la carpeta del backend:
composer install,copy .env.example .env,php artisan key:generate. - En
.envponAPP_URL,DB_DATABASEyMAIL_MAILER=log(los correos quedan enstorage/logs/laravel.log). php artisan migrate --seedy, 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:
| Tipo | Nombre | Valor |
|---|---|---|
| A | api | La IP del servidor |
| A | app | La IP del servidor |
| A | updates | La IP del servidor |
Si usas Cloudflare, deja las nubes en «Solo DNS» (gris) hasta tener el HTTPS funcionando. Para comprobar la propagación:
nslookup {{apiHost}}
nslookup {{webHost}}
nslookup {{updHost}}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 nginxAntes 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
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.
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).
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
- 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.
- Entra a esa cuenta y activa la verificación en dos pasos (myaccount.google.com · Seguridad).
- 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. - 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.
| Dato | Valor |
|---|---|
| MAIL_HOST | |
| MAIL_PORT | |
| MAIL_USERNAME | |
| MAIL_PASSWORD | La contraseña de aplicación (va en secrets.json) |
Zoho Mail
- Crea la cuenta en zoho.com/mail con tu dominio y verifica el dominio con el registro que te pida.
- Crea el buzón y otro (o un alias) para .
- En el buzón: Configuración · Seguridad · Contraseñas específicas de aplicación, genera una para «Kynetic». Esa es
MAIL_PASSWORD. - Confirma el servidor en Zoho · Configuración · Mail · Acceso IMAP/SMTP. Zoho usa
smtp.zoho.com(cuentas .com) osmtp.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.
| Dato | Valor |
|---|---|
| MAIL_HOST | |
| MAIL_PORT | |
| MAIL_USERNAME |
Correo del hosting (cPanel)
- cPanel · Cuentas de correo · Crear: y . Guarda sus contraseñas.
- 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.
| Dato | Valor |
|---|---|
| MAIL_HOST | |
| MAIL_PORT | |
| MAIL_USERNAME | |
| MAIL_PASSWORD | La contraseña del buzón (va en secrets.json) |
#). 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
- Crea la cuenta en brevo.com (plan gratuito: unos 300 correos al día).
- Añade y autentica tu dominio (Senders, Domains & Dedicated IPs · Domains): Brevo te da los registros DKIM y de verificación para el DNS.
- Añade y como remitentes verificados.
- 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.
| Dato | Valor |
|---|---|
| MAIL_HOST | |
| MAIL_PORT | |
| MAIL_USERNAME | El inicio de sesión SMTP de Brevo. Escríbelo en backend_env de client.json (fase de archivos) |
| MAIL_PASSWORD | La 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.
| Registro | Nombre | Valor |
|---|---|---|
| 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. |
| DKIM | Lo 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 | |
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
- Entra a console.firebase.google.com con una cuenta de Google que el club conserve (no la personal de un miembro).
- Crear un proyecto, con el nombre del club. Google Analytics es opcional; puedes desactivarlo.
- Al terminar, pulsa el engranaje · Configuración del proyecto · pestaña General.
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 conAIza)
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
- 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). - Anota el Key ID (10 caracteres) y tu Team ID (en la cuenta, sección Membership).
- En Firebase · Configuración · Cloud Messaging · Configuración de la app de Apple: sube el .p8 con el Key ID y el Team ID.
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.
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):
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
sioyes. - Contraseña de la clave: pulsa Enter para usar la misma que el almacén (más simple).
.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}}.jkskeyAlias 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
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}}
}
}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.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).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:
cd "{{backendDir}}"
php artisan key:generate --showImprime 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": ""
}DB_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».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.
| Repositorio | Qué hace su pipeline | Cuándo corre |
|---|---|---|
| App Flutter | Analiza, prueba y revisa imports de plataforma | Cada push |
| Backend | Pruebas; y deploy-client sincroniza, migra y recarga cachés | Pruebas en cada push; el despliegue lo lanzas tú |
| Builds | Copia por rsync el APK, latest.json y la web al servidor de actualizaciones | Cada 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):
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:
- Pipelines · Settings · activa Enable Pipelines.
- Pipelines · SSH keys · Generate keys. Aparece una clave pública (una sola línea que empieza con
ssh-rsa); cópiala completa. - 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 desplieguemkdir -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. - 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.
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:
| Variable | Valor |
|---|---|
| DEPLOY_USER | |
| DEPLOY_HOST | |
| DEPLOY_PORT | |
| DEPLOY_PATH |
.htaccess). Que sea solo la carpeta de updates, nunca la del backend ni tu carpeta personal.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
| Variable | Valor |
|---|---|
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:
- Edita la constante
SUPER_ADMINcon los datos de la persona que será dueña del club. - Revisa la contraseña inicial (en
run()conHash::make('…'), o enSUPER_ADMIN_PASSWORD_HASHsi tu versión usa un hash). Para generar un hash:php artisan tinkeryecho bcrypt('LA_CONTRASEÑA');. - Haz commit y push al repositorio del backend antes del primer despliegue.
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
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
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.jsonLa 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.
proc_open, base de datos o .env).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.
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:cacheEl último seeder imprime Super admin creado: …. Con eso ya puedes entrar y operar.
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 necesitas | Para qué sirve |
|---|---|
| DemoSeeder | Datos 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.
php artisan storage:link falla en hosting compartido (Call to undefined function exec()). Créalo a mano:
cd {{appPath}}
ln -s "$(pwd)/storage/app/public" public/storage
ls -l public/storagerm 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:
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:
{
"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
Guardar los íconos del club
La primera vez, descarga las dos dependencias nuevas del script:
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.
Qué se genera al compilar
| Dónde | Qué |
|---|---|
| Android | Los 5 tamaños del ícono del launcher (48 a 192 px). |
| iPhone | Los 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 / PWA | favicon, Icon-192, Icon-512 y las dos versiones maskable. |
| Windows | app_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:
"branding": {
"accent": "#ff6a2b",
"background": "#ffffff",
"themeColor": "#120E0B",
"shortName": "{{name}}"
},| Campo | Qué hace |
|---|---|
| accent | Color de los botones de la página de descargas. |
| background | Color que rellena la transparencia del ícono de iPhone y el margen del ícono maskable. Blanco si no lo defines. |
| themeColor | Color de la barra del navegador y de la PWA instalada. |
| exeName | Nombre del archivo .exe de Windows, solo letras, números y guion bajo. Si no lo defines, se usa el slug. |
| shortName | Nombre 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:
{{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.
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 doctordebe 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%\Programssin pedir administrador, crea acceso en el menú Inicio y el escritorio, y se desinstala desde Configuración. Además se publica un.zipportá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
namedeclient.json, y el ejecutable usa el slug con guiones bajos (por ejemplocon guiones bajos) o elbranding.exeNameque definas. Los archivos de Windows del proyecto se modifican solo durante la compilación y luego se restauran.
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únPEGA_…(la vista de la fase de archivos lo marca).y el.jksen su sitio.flutter pub getejecutado al menos una vez.urls.updatesapuntando 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
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.
--push, un push a Bitbucket. En Bitbucket · Pipelines, el pipeline de builds corre y termina en verde.Qué sale de cada destino
| Destino | Archivo | Para qué |
|---|---|---|
| apk | Instalación directa y actualizaciones desde la app. Va al servidor. | |
| aab | Google Play. Queda local, no se sube a git ni al servidor. | |
| web | PWA con el service worker de push ya rellenado con los datos del club. | |
| windows | App 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. | |
| ipa | App 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.
tool\publicar_todo.bat "Novedades" equivale a build --all --targets apk,aab,web --notes "Novedades" --push.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
- 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.
- Crear app: nombre, idioma, tipo «App», gratuita.
- 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
- 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).
- Certificates, Identifiers & Profiles · Identifiers · «+» · App IDs: crea uno con el Bundle ID
y activa la capacidad Push Notifications. - appstoreconnect.apple.com · Mis apps · «+» · Nueva app, con ese Bundle ID.
Compilar y subir el IPA (en un Mac)
- En el Mac abre
ios/Runner.xcworkspacecon 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. - Compila con
. - 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.
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
- Con la versión instalada, compila y publica otra con las opciones por defecto (sube el número de build).
- 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).
- 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
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.
| Variable | Archivo | Qué es y de dónde sale | La rellena |
|---|