5Stack uses Pinia for centralized state management with Vue 3’s Composition API. Stores are reactive, type-safe, and integrate seamlessly with GraphQL subscriptions.
Pinia Overview
Pinia is the official state management library for Vue 3, offering:
Composition API Define stores using setup() syntax with full TypeScript support
Reactive State Automatic reactivity using Vue’s ref() and computed()
DevTools Integration Time-travel debugging and state inspection
HMR Support Hot Module Replacement during development
Store Architecture
The application has several specialized stores:
stores/
├── AuthStore.ts # Authentication and user session
├── MatchmakingStore.ts # Matchmaking, lobbies, and friends
├── MatchLobbyStore.ts # Active matches and tournaments
├── NotificationStore.ts # Notifications and alerts
├── SearchStore.ts # Global search state
├── FileManagerStore.ts # File browser for demos/logs
├── ApplicationSettings.ts # App configuration and settings
└── TeamLobbyStore.ts # Team management
AuthStore
The AuthStore manages user authentication and session state.
Store Definition
// stores/AuthStore.ts
import { ref , computed } from "vue" ;
import { defineStore , acceptHMRUpdate } from "pinia" ;
import { generateQuery , generateSubscription } from "~/graphql/graphqlGen" ;
import { meFields } from "~/graphql/meGraphql" ;
import getGraphqlClient from "~/graphql/getGraphqlClient" ;
import type { GraphQLTypes , InputType } from "~/generated/zeus" ;
export const useAuthStore = defineStore ( 'auth' , () => {
// State
const me = ref < InputType < GraphQLTypes [ 'players' ], typeof meFields >>();
const hasDiscordLinked = ref < boolean >( false );
// Initialize other stores
useSearchStore ();
useMatchmakingStore ();
useNotificationStore ();
useApplicationSettingsStore ();
// Actions and getters defined below...
return {
me ,
getMe ,
hasDiscordLinked ,
isUser ,
isAdmin ,
isRoleAbove ,
};
});
The store initializes dependent stores (matchmaking, notifications, etc.) to ensure proper setup order.
Role-Based Access Control
const roleOrder = [
e_player_roles_enum . user ,
e_player_roles_enum . verified_user ,
e_player_roles_enum . streamer ,
e_player_roles_enum . match_organizer ,
e_player_roles_enum . tournament_organizer ,
e_player_roles_enum . administrator ,
];
function isRoleAbove ( role : e_player_roles_enum ) {
if ( ! me . value ) return false ;
const meRoleIndex = roleOrder . indexOf ( me . value . role );
const roleIndex = roleOrder . indexOf ( role );
return meRoleIndex >= roleIndex ;
}
// Computed role checks
const isAdmin = computed (
() => me . value ?. role === e_player_roles_enum . administrator
);
const isMatchOrganizer = computed (
() => me . value ?. role === e_player_roles_enum . match_organizer
);
Fetching User Data
async function getMe () : Promise < boolean > {
// Helper function for subscriptions
function subscribeToMe ( steam_id : string , callback : () => void ) {
const subscription = getGraphqlClient (). subscribe ({
query: generateSubscription ({
players_by_pk: [
{ steam_id },
meFields ,
],
}),
});
subscription . subscribe ({
next : ({ data }) => {
me . value = data ?. players_by_pk ;
if ( me . value ) {
// Subscribe to user-specific data
useMatchLobbyStore (). subscribeToMyMatches ();
useMatchLobbyStore (). subscribeToLiveMatches ();
// Conditionally subscribe based on role
if ( isRoleAbove ( e_player_roles_enum . match_organizer )) {
useMatchLobbyStore (). subscribeToManagingMatches ();
}
}
callback ();
},
});
}
return await new Promise ( async ( resolve ) => {
try {
// Initial query to get basic user info
const response = await getGraphqlClient (). query ({
query: generateQuery ({
me: {
role: true ,
steam_id: true ,
discord_id: true ,
},
}),
fetchPolicy: 'network-only' ,
});
if ( ! response . data . me ) {
resolve ( false );
return ;
}
// Connect WebSocket
socket . connect ();
hasDiscordLinked . value = !! response . data . me . discord_id ;
// Wait for WebSocket to connect
const wsClient = useNuxtApp (). $wsClient ;
await new Promise < void >(( resolveWs ) => {
const timeout = setTimeout (() => {
dispose ();
resolveWs ();
}, 10000 );
const dispose = wsClient . on ( 'connected' , () => {
clearTimeout ( timeout );
dispose ();
resolveWs ();
});
});
// Subscribe to real-time updates
subscribeToMe ( response . data . me . steam_id , () => {
resolve ( true );
});
} catch ( error ) {
console . warn ( 'auth failure' , error );
resolve ( false );
}
});
}
Query basic user info (role, steam_id, discord_id)
If user exists, connect WebSocket for subscriptions
Wait for WebSocket connection to establish
Subscribe to real-time user updates via GraphQL subscription
Initialize role-specific subscriptions (matches, tournaments, etc.)
Hot Module Replacement
if ( import . meta . hot ) {
import . meta . hot . accept ( acceptHMRUpdate ( useAuthStore , import . meta . hot ));
}
MatchmakingStore
The MatchmakingStore handles matchmaking queues, lobbies, friends, and latency testing.
State Definition
// stores/MatchmakingStore.ts
export const useMatchmakingStore = defineStore ( 'matchmaking' , () => {
// Online players
const playersOnline = ref ([]);
const onlinePlayerSteamIds = ref < string []>([]);
// Matchmaking state
const joinedMatchmakingQueues = ref <{
details ?: {
totalInQueue : number ;
type : e_match_types_enum ;
regions : Array < string >;
};
confirmation ?: {
matchId : string ;
isReady : boolean ;
expiresAt : string ;
confirmed : number ;
confirmationId : string ;
type : e_match_types_enum ;
region : string ;
players : number ;
};
}>({
details: undefined ,
confirmation: undefined ,
});
// Friends and lobbies
const friends = ref ([]);
const lobbies = ref ([]);
const matchInvites = ref ([]);
// Region/latency state
const regionStats = ref <
Partial < Record < string , Partial < Record < e_match_types_enum , number >>>>
> ({});
const latencies = ref ( new Map < string , number []>());
const storedRegions = ref < string []>([]);
const playerMaxAcceptableLatency = ref ( 75 );
// Actions defined below...
return {
friends ,
onlineFriends ,
lobbies ,
currentLobby ,
latencies ,
refreshLatencies ,
inviteToLobby ,
// ...
};
});
Real-Time Friends List
const subscribeToFriends = async ( mySteamId : bigint ) => {
const subscription = getGraphqlClient (). subscribe ({
query: generateSubscription ({
my_friends: [
{},
{
elo: true ,
name: true ,
role: true ,
steam_id: true ,
avatar_url: true ,
status: true ,
player: {
is_in_lobby: true ,
is_in_another_match: true ,
lobby_players: [
{
limit: 1 ,
where: {
lobby: {
_not: {
players: {
steam_id: { _eq: $ ( 'mySteamId' , 'bigint!' ) },
},
},
access: {
_in: [
e_lobby_access_enum . Friends ,
e_lobby_access_enum . Open ,
],
},
},
},
},
{
lobby_id: true ,
lobby: {
id: true ,
players: [
{},
{
player: {
name: true ,
steam_id: true ,
avatar_url: true ,
},
},
],
},
},
],
},
},
],
}),
variables: { mySteamId },
});
subscription . subscribe ({
next : ({ data }) => {
friends . value = data . my_friends ;
},
});
};
Computed Values
const onlineFriends = computed (() => {
return friends . value ?. filter (( friend : any ) => {
if ( friend . status === 'Pending' ) return false ;
return onlinePlayerSteamIds . value . includes ( friend . steam_id );
});
});
const currentLobby = computed (() => {
return lobbies . value . find (( lobby : any ) => {
return lobby . id === useAuthStore (). me ?. current_lobby_id ;
});
});
const lobbyInvites = computed (() => {
const me = useAuthStore (). me ;
if ( ! lobbies . value ) return [];
return lobbies . value . filter (( lobby : any ) => {
return lobby . id !== me ?. current_lobby_id ;
});
});
Watchers for Reactive Updates
// Watch for auth changes
watch (
() => useAuthStore (). me ,
( me ) => {
if ( me ) {
subscribeToFriends ( me . steam_id );
subscribeToMatchInvites ( me . steam_id );
subscribeToLobbies ( me . steam_id );
}
},
{ immediate: true },
);
// Watch for online player changes
watch ( onlinePlayerSteamIds , ( newSteamIds , oldSteamIds ) => {
if (
newSteamIds . length !== oldSteamIds . length ||
! newSteamIds . every (( id , index ) => id === oldSteamIds [ index ])
) {
queryPlayers ();
}
});
Latency Testing with WebRTC
const isRefreshing = ref ( false );
async function refreshLatencies () {
if ( isRefreshing . value ) return ;
isRefreshing . value = true ;
resetLatencies ();
await Promise . all (
useApplicationSettingsStore (). availableRegions . map (( region ) =>
getLatency ( region . value )
)
);
isRefreshing . value = false ;
}
async function getLatency ( region : string ) {
return new Promise ( async ( resolve ) => {
setTimeout (() => resolve ( undefined ), 5000 );
try {
const buffer = new Uint8Array ([ 0x01 ]). buffer ;
const datachannel = await webrtc . connect ( region , ( data ) => {
if ( data === "" ) {
datachannel . send ( buffer );
return ;
}
const event = JSON . parse ( data );
if ( event . type === "latency-results" ) {
datachannel . close ();
latencies . value . set ( region , event . data );
}
});
datachannel . send ( "latency-test" );
} catch ( error ) {
console . error ( `Failed to get latency for ${ region } ` , error );
resolve ( undefined );
}
});
}
LocalStorage Integration
// Load preferred regions from localStorage
const localStoragePreferredRegions = localStorage . getItem ( PREFERRED_REGIONS_KEY );
const storedRegions = ref < string []>(
localStoragePreferredRegions
? JSON . parse ( localStoragePreferredRegions )
: []
);
// Load latencies for each region
useApplicationSettingsStore (). availableRegions . forEach (( region ) => {
const savedLatency = localStorage . getItem ( REGION_LATENCY_PREFIX + region . value );
if ( savedLatency ) {
latencies . value . set ( region . value , JSON . parse ( savedLatency ));
}
});
function togglePreferredRegion ( region : string ) {
const index = storedRegions . value . indexOf ( region );
if ( index !== - 1 ) {
storedRegions . value . splice ( index , 1 );
} else {
storedRegions . value . push ( region );
}
localStorage . setItem (
PREFERRED_REGIONS_KEY ,
JSON . stringify ( storedRegions . value . filter ( Boolean ))
);
}
Using Stores in Components
Setup Syntax
< script setup lang = "ts" >
import { useAuthStore } from '~/stores/AuthStore' ;
import { useMatchmakingStore } from '~/stores/MatchmakingStore' ;
const authStore = useAuthStore ();
const matchmakingStore = useMatchmakingStore ();
// Access state directly (reactive)
const userName = computed (() => authStore . me ?. name );
const onlineFriends = computed (() => matchmakingStore . onlineFriends );
// Call actions
const handleInvite = async ( steamId : string ) => {
await matchmakingStore . inviteToLobby ( steamId );
};
</ script >
< template >
< div >
< p > Welcome, {{ userName }} </ p >
< p > {{ onlineFriends . length }} friends online </ p >
</ div >
</ template >
Options API (if needed)
< script lang = "ts" >
export default {
computed: {
me () {
return useAuthStore (). me ;
},
} ,
} ;
</ script >
Store Patterns
Pattern: Initialize on Auth
Many stores initialize subscriptions when the user logs in:
watch (
() => useAuthStore (). me ,
( me ) => {
if ( me ) {
// Start subscriptions
subscribeToData ( me . steam_id );
}
},
{ immediate: true },
);
Pattern: Cross-Store Communication
// Store A reads from Store B
const currentLobby = computed (() => {
const me = useAuthStore (). me ; // Access another store
return lobbies . value . find ( l => l . id === me ?. current_lobby_id );
});
Pattern: GraphQL Subscription → Reactive State
function subscribeToData () {
const subscription = getGraphqlClient (). subscribe ({
query: generateSubscription ({ /* ... */ }),
});
subscription . subscribe ({
next : ({ data }) => {
// Update reactive state
myState . value = data . something ;
},
});
}
Best Practices
Always type your state using generated GraphQL types: import type { InputType , GraphQLTypes } from '~/generated/zeus' ;
const me = ref < InputType < GraphQLTypes [ 'players' ], typeof meFields >>();
Computed for Derived State
Use computed() for values derived from state: const onlineFriends = computed (() => {
return friends . value . filter ( f =>
onlinePlayerSteamIds . value . includes ( f . steam_id )
);
});
Watchers for Side Effects
Use watch() to react to state changes: watch (() => authStore . me , ( me ) => {
if ( me ) {
initializeSubscriptions ();
}
});
Unsubscribe when no longer needed: let unsubscribe : (() => void ) | undefined ;
function subscribe () {
const sub = getGraphqlClient (). subscribe ( /* ... */ );
unsubscribe = sub . subscribe ( /* ... */ ). unsubscribe ;
}
onUnmounted (() => {
unsubscribe ?.();
});
Next Steps
GraphQL API Learn how to query and subscribe to data
Components Build reactive components with stores