Programación de publicaciones — Referencia técnica
← Volver a Programación de publicaciones
La programación de publicaciones (Fase D.6 / roadmap 4.14) agrega un scheduledAt nullable a las publicaciones más un worker cron que libera las publicaciones que llegan a su hora. Antes de esto, las publicaciones solo tenían isPublished: Boolean (por defecto true) y no existía ningún concepto de publicación futura.
Dónde vive esto
Backend
apps/backend/database/migrations/20260717090000-add-scheduled-at-to-posts.js— agregascheduled_at(timestamp nullable) + un índice a la tablapost(idempotente).apps/backend/database/models/Post.js— el atributoscheduledAt.apps/backend/validators/post.validator.js—validateCreateInputaceptascheduledAt, rechaza fechas malformadas y cualquier valor a más de ~1 año.apps/backend/managers/post-managers/post.manager.js—createPost/createStorymantienen una publicación/historia programada a futuro conisPublished: false;getScheduledPosts(las propias del dueño),cancelScheduledPost(solo el dueño, solo mientras siga programada — elimina la fila),updateScheduledPost(solo el dueño, solo mientras siga programada — editatext/visibility/scheduledAt), ypublishDueScheduledPosts(el núcleo del worker: cambia las publicaciones vencidas aisPublished: true, limpiascheduledAt).apps/backend/data-access-services/post/post.access-service.js—getScheduledByUser,getDueScheduled, ygetByUser: quienes visitan el perfil sin ser el dueño solo ven publicaciones conisPublished: true, así que una publicación programada nunca se filtra en la cuadrícula de perfil de otra persona antes de su hora; el dueño, al ver su propia cuadrícula, además ve sus propias publicaciones programadas (con una insignia con la hora de liberación) para no tener que abrir la vista dedicada de publicaciones programadas para ver qué hay en cola.apps/backend/services/scheduled-post-publisher.service.js— el job cron (* * * * *, cada minuto), registrado desdeindex.jsal arrancar, replicando el patrón de node-cron ya existente desubscription-reminder.service.js.apps/backend/graphql/types/post.type.js+resolvers/post.resolver.js—scheduledAtenPost,PostCreateInputyStoryCreateInput, la querymyScheduledPosts, y las mutationscancelScheduledPost/updateScheduledPost(ScheduledPostUpdateInput).
Frontend
apps/frontend-nextjs/src/components/CreatePostModal.tsx— un interruptor "Programar" que abre un panelDateTimePickerestilo calendario (componente compartido de@repo/ui, verpackages/ui/src/DateTimePicker/DateTimePicker.tsx); pasascheduledAt(ISO) solo cuando la hora elegida realmente está en el futuro, y renombra el botón de redacción a "Programar".apps/frontend-nextjs/src/components/stories/CreateStoryModal.tsx— mismo patrón deDateTimePickerpara las historias; pasascheduledAtencreateStory.apps/frontend-nextjs/src/page-components/settings/ScheduledPostsPage.tsx(ruta/settings/scheduled-posts, enlazada desde el menú de configuración) — listamyScheduledPostscon la hora programada de cada publicación, un botón de editar (updateScheduledPost, texto + fecha/hora medianteDateTimePicker) y un botón de cancelar (cancelScheduledPost).apps/frontend-nextjs/src/page-components/PublicProfilePage.tsxyapps/frontend-nextjs/src/components/PostModal.tsx— muestran una insignia con la hora de liberación programada en una publicación que el dueño ve en su propia cuadrícula de perfil / modal de detalle de la publicación (!post.isPublished && post.scheduledAt).
Checklist de implementación técnica
-
scheduledAtenPost/PostCreateInput;createPostmantiene sin publicar las publicaciones futuras - Query
myScheduledPosts—ScheduledPostsPage.tsx - Mutation
cancelScheduledPost— botón de cancelar en la misma página - Mutation
updateScheduledPost(ScheduledPostUpdateInput: text/visibility/scheduledAt) — botón de editar en la misma página - Worker cron
scheduled-post-publisher.service.js— publica las publicaciones vencidas cada minuto -
getByUseroculta las publicaciones programadas a quienes visitan sin ser el dueño; el dueño ve sus propias publicaciones programadas en su cuadrícula de perfil, con una insignia con la hora de liberación - Programación de historias —
StoryCreateInput.scheduledAt(CreateStoryModal.tsx); la ventana de 24hexpiresAtde la historia empieza cuando el barrido la publica, no al crearla - Programación de clips — un clip es simplemente una publicación cuyo único medio es un video (
type: 'clip', establecido automáticamente encreatePost), así que ya queda cubierto por la programación normal de publicaciones; no hay una interfaz de programación de clips separada
API de GraphQL
# Programar una publicación: pasa un timestamp ISO futuro. Omítelo (o pasa
# una hora pasada) para publicar inmediatamente.
mutation CreateScheduledPost($input: PostCreateInput!) {
createPost(input: $input) { id scheduledAt isPublished }
}
# input: { text: "...", visibility: public, scheduledAt: "2026-08-01T15:00:00Z" }
# Las publicaciones programadas (aún no publicadas) propias de quien llama, la más próxima primero.
query MyScheduledPosts($limit: Int, $offset: Int) {
myScheduledPosts(limit: $limit, offset: $offset) {
id text scheduledAt visibility
media { mediaUrl thumbnailUrl mediaType }
}
}
# Cancelar antes de que se publique (solo el dueño, solo mientras siga programada).
mutation CancelScheduledPost($postId: ID!) {
cancelScheduledPost(postId: $postId)
}
# Editar el texto, la visibilidad y/o la hora de una publicación aún programada
# (solo el dueño, solo mientras siga programada; scheduledAt debe estar en el futuro).
mutation UpdateScheduledPost($postId: ID!, $input: ScheduledPostUpdateInput!) {
updateScheduledPost(postId: $postId, input: $input) { id text scheduledAt }
}
# Programar una historia de la misma manera; su ventana de 24h empieza cuando se publica.
mutation CreateScheduledStory($input: StoryCreateInput!) {
createStory(input: $input) { id visibility }
}
Cómo funciona la liberación
createPost/createStory guardan una publicación (o historia) programada a futuro con isPublished: false y el scheduledAt elegido. Las queries de feed y las cuadrículas de perfil de otros usuarios filtran todas por isPublished: true, así que nadie más que el dueño la ve antes de su hora. Cada minuto, scheduled-post-publisher.service.js llama a post.manager.js#publishDueScheduledPosts, que encuentra las publicaciones donde isPublished = false AND scheduledAt <= now (getDueScheduled, respaldado por el índice scheduled_at) y actualiza cada una a isPublished: true, scheduledAt: null — y, para una historia programada (type === 'story'), también establece un nuevo expiresAt a 24h desde el momento real de publicación, ya que una historia programada no inicia su ventana de 24h hasta que realmente se libera. A partir de ese momento la publicación/historia es indistinguible de una publicada normalmente. La latencia en el peor caso es de ~60 segundos después del minuto programado. Cada publicación se actualiza de forma independiente, así que un fallo no bloquea el resto del lote.