02 — someli-platform stack¶
Two apps in one repo: a Nuxt 2 SPA and a React + Polotno sub-project. Plus a thick layer of CDN-loaded scripts.
The two apps¶
| App | Lives in | Stack |
|---|---|---|
| Main app (the customer-facing SaaS UI) | repo root | Nuxt 2.18 / Vue 2.7 / SPA mode |
| Visual editor | polotno-editor/ |
React 18 + Polotno + MobX-State-Tree (Parcel build) |
The two are completely separate Node projects (separate package.json, separate node_modules, separate lockfiles). They share no source code. The editor is built into a single polotno-bundle.js (+ .css) at the repo root, and the Nuxt app imports it as an ES module:
Main app stack¶
| Concern | Choice | Version |
|---|---|---|
| Framework | Nuxt 2 | 2.18.1 (CLI), 2.15.8 (core) |
| View library | Vue 2 | 2.7.16 (pinned) |
| Mode | SPA — ssr: false in nuxt.config.js |
— |
| State | Vuex 3 (non-strict mode) | — |
| HTTP client | @nuxtjs/axios |
^5.13.6 |
| Auth | @nuxtjs/auth + local strategy |
^4.9.1 |
| Cookies | cookie-universal-nuxt |
^2.2.2 |
| UI library | Bootstrap-Vue 2 + Bootstrap 4 classes | ^2.23.1 |
| Build | Nuxt 2's built-in webpack | — |
| Notifications | vue-snotify |
— |
| Forms | filepond + plugins |
— |
| Code style | Prettier (.prettierrc) |
— |
| Tests | None. package.json test script just exit 1. |
— |
"SPA mode" means¶
ssr: false in nuxt.config.js makes Nuxt a build-time SPA generator, not a server. There is no Node-side rendering at request time. In production, yarn build produces a static SPA, and nginx serves it.
Junior gotcha: despite being a Nuxt app,
asyncDataandfetchlifecycle hooks run client-side only. Don't expect a SSR-style data-fetch behaviour.
Server middleware (small, easy to miss)¶
api/ contains tiny Nuxt serverMiddleware handlers (small Express routes injected via nuxt.config.js → serverMiddleware). The only currently-live endpoint is GET /api/youtube-videos in api/index.js. The rest is commented-out scaffolding.
Junior gotcha:
api/here is not the customer-facing API backend (that'ssomeli-api). It is a Nuxt-internal serverMiddleware. Easy to confuse.
Polotno sub-project stack¶
| Concern | Choice |
|---|---|
| Framework | React 18 |
| Editor SDK | Polotno (polotno for client, polotno-node lives in the BE) |
| State | MobX-State-Tree |
| Build | Parcel |
| Output | polotno-bundle.js (+ .css) at the parent repo root |
The Polotno editor is a separate React SPA that builds into a single JS+CSS bundle. The Nuxt app imports that bundle.
The customer Polotno editor and the designer Polotno editor have diverged. See
../../audit/CODE-OVERLAP-MATRIX.md§ 6. Fixes do not auto-port.
For the full Polotno integration story: ../../audit/someli-platform/06-polotno-integration.md.
CDN-loaded scripts (head injection)¶
nuxt.config.js → head.script injects ~10 <script> tags loaded from external CDNs (rather than bundled):
- jQuery — used by Bootstrap 4 components, slick-carousel, etc.
- Bootstrap 4 bundle JS — Bootstrap-Vue brings the CSS; the JS comes from CDN
- Stripe.js — for legacy Stripe billing
- Paddle.js — for Paddle billing
- Microsoft Clarity — user-session analytics
- FontAwesome — icons
slick-carousel— carousel JSFlagmeister— country flagsSnowfall— winter / seasonal effect
Junior gotcha:
- None of these are subject to SRI (no
integrityattributes). A CDN compromise → script injection. Known platform-wide finding.- CDN dependencies are not in
package.json. Searchingpackage.jsonfor "jquery" finds nothing — but jQuery is loaded globally. Always look atnuxt.config.jsfor the full dependency picture.- CDN scripts may load synchronously and block first paint. Performance budget is partly outside your bundle control.
Vuex store layout¶
store/ is the single most important state location.
| Module | What |
|---|---|
store/index.js |
Root module (export const strict = false) |
store/api.js |
Cache layer wrapping ~12 backend endpoints with a 5-minute TTL — fetchSubscription, fetchUserDetails, fetchSettings, fetchPosts(page), fetchFavPosts(page), … |
store/post/ |
Post lifecycle state |
store/dashboard/ |
Dashboard analytics |
store/chat/ |
Chat / messaging |
store/common/ |
Common shared state |
store/plan/ |
Billing plans |
store/reach/ |
Reach / impressions |
store/onboarding/ |
Onboarding flow |
store/leaderboard/ |
Leaderboard data |
store/modules/sitemap/ |
Sitemap utilities |
Components should prefer dispatching
store/api/fetch*actions over calling$axiosdirectly. The caching saves real API load and prevents UI flicker.
See ../../audit/someli-platform/05-state-management.md.
Identity, cookies, auth¶
| Concern | Where |
|---|---|
| Strategy | @nuxtjs/auth v4, local strategy |
| Login endpoint | webauthenticate (defined in nuxt.config.js → auth.strategies.local.endpoints.login) |
| User-fetch endpoint | me |
| Logout | Disabled at the strategy level (logout: false). The app calls app.$auth.logout() directly and clears cookies + localStorage itself. |
| Token storage | Cookie (cookie-universal-nuxt) |
| Account ID | Cookie (usedetail) |
Identity lives in cookies, not in the Vuex store. The store sometimes mirrors cookie values, but cookies are the source of truth.
See ../../audit/someli-platform/03-auth-and-sessions.md.
Axios interceptor — adds auth headers¶
middleware/axios.js installs an axios request interceptor that:
- Adds the
tokenheader (from cookie) — only for URLs starting with/auth/ - Adds the
accountIdheader (fromusedetailcookie) — also only for/auth/ - Adds a base64-encoded
apptypeheader on every request
Consequence: if you call a backend endpoint not under
/auth/, your request will be unauthenticated from the frontend's perspective. This is the opposite of what many backends assume — be careful which prefix your new endpoint sits under.
Router middleware chain¶
Declared in nuxt.config.js → router.middleware, runs on every navigation:
middleware/redirect.js— decides where the user belongs (post-login routing)middleware/axios.js— installs the axios interceptormiddleware/validUrl.js— records last-known-good pathmiddleware/appendUTM.js— decorates outbound links with UTM parametersmiddleware/appendUrl.js— rewrites unprefixed URLs to/<accountId>/...for a hardcoded allow-list (/contentplanner,/dashboard/*,/billing,/userSetting, …)
Account-scoped routing: every meaningful URL is prefixed with
accountId./contentplanneris intentionally allowed as a bare URL;appendUrlwill rewrite it to/<accountId>/contentplanner. The router middleware is doing real work — read it before "fixing" a redirect bug.
See ../../audit/someli-platform/04-routing.md.
Components¶
| Auto-registered | Yes (components: true in nuxt.config.js) |
| Folder | components/, with subdirectories grouping related components |
| Notable subfolders | Calender/ (sic), Charts/, Dashboard/, Buttons/, Cards/, Sidebars/ |
Auto-registration means you can use any component without an import statement — Nuxt picks it up by filename.
Layouts¶
| Layout | Used for |
|---|---|
layouts/default.vue |
Main authenticated UI |
layouts/error.vue |
404 / error pages |
layouts/onBoard.vue |
Onboarding flow |
layouts/someliNetwork.vue |
"Someli Network" feature |
layouts/specialOffer.vue |
Special-offer landing pages |
layouts/fourOhOne.vue |
401 redirect page |
Pages opt in via the layout option:
Plugins¶
plugins/ initialises client-only singletons. Most are declared ssr: false or mode: 'client' because the app doesn't SSR.
Examples: gtm.js (Google Tag Manager via @nuxtjs/gtm), hotjar.js, fullstory.js, clarity.js, salesforceiq.js, vue-snotify.js, Polotno-related client-only libs.
Build, lint, test¶
| Build | yarn build → static SPA in .nuxt/dist/ |
| Lint | Not configured |
| Format | yarn format runs Prettier |
| Tests | None |
| CI | Jenkins only (no GitHub Actions in this repo) |
| Deploy | Jenkins SSH-deploys to AWS Lightsail; production frontends mount node_modules from EFS at runtime (see start.sh) |
Consequence for you: type and lint errors do not fail your PR. Manual verification in the browser is mandatory.
Next¶
→ 03-architecture.md — the folder map and request lifecycle.