diff --git a/apps/docs/.env.example b/apps/docs/.env.example
new file mode 100644
index 0000000..846b3b0
--- /dev/null
+++ b/apps/docs/.env.example
@@ -0,0 +1,51 @@
+## --- Notes ----------------------------------------------------------------------------------- */
+
+## -i- The `.env.example` file can be copied into `.env.local` using `npx turbo env:local`
+## -i- For development, staging & production environments, check the next.js docs:
+## -i- https://nextjs.org/docs/app/building-your-application/configuring/environment-variables
+
+## -i- Note that you should treat environment variables as if they could be inlined in your bundle during builds & deployments
+## -i- This means dynamically retrieving environment variables from e.g. `process.env[someKey]` might not work
+## -i- It also means that you should never prefix with `NEXT_PUBLIC_` for sensitive / private keys
+
+## -i- We suggest that for each environment variable you add here, you also add an entry in `appConfig.ts`
+## -i- There, you can add logic like ```envValue: process.env.NEXT_PUBLIC_ENV_KEY || process.env.EXPO_PUBLIC_ENV_KEY```
+## -i- Where you would only define the NEXT_PUBLIC_ prefixed versions here in `.env.local` locally and using Next.js UI for deployed envs
+## -i- For environment variables you only be available server-side, you can omit `NEXT_PUBLIC_`
+
+NEXT_PUBLIC_APP_ENV=next
+
+## --- General --------------------------------------------------------------------------------- */
+## -i- Env vars that should always be present & the same locally, independent of the simulated environment
+## --------------------------------------------------------------------------------------------- */
+
+APP_SECRET="your-secret-here" # used for signing header context, generate a random string
+
+## --- LOCAL ----------------------------------------------------------------------------------- */
+## -i- Defaults you might want to switch out for local development by commenting / uncommenting
+## --------------------------------------------------------------------------------------------- */
+
+NEXT_PUBLIC_BASE_URL=http://localhost:3000
+NEXT_PUBLIC_BACKEND_URL=http://localhost:3000
+NEXT_PUBLIC_API_URL=http://localhost:3000/api
+NEXT_PUBLIC_GRAPH_URL=http://localhost:3000/api/graphql
+
+# DB_URL= # TODO: Add DB layer connection for full local dev...
+
+## --- DEV ------------------------------------------------------------------------------------- */
+# -i- Uncomment while on development branch to simulate the dev environment
+## --------------------------------------------------------------------------------------------- */
+
+# DB_URL= # TODO: Add DB layer connection for the dev environment...
+
+## --- STAGE ----------------------------------------------------------------------------------- */
+# -i- Uncomment while on staging branch to simulate the stage environment
+## --------------------------------------------------------------------------------------------- */
+
+# DB_URL= # TODO: Add DB layer connection for the stage environment...
+
+## --- PROD ------------------------------------------------------------------------------------ */
+# -i- Uncomment while on main branch to simulate the production environment
+## --------------------------------------------------------------------------------------------- */
+
+# DB_URL= # TODO: Add DB layer connection for the production environment...
diff --git a/apps/docs/babel.config.js b/apps/docs/babel.config.js
new file mode 100644
index 0000000..91dfe76
--- /dev/null
+++ b/apps/docs/babel.config.js
@@ -0,0 +1,14 @@
+// babel.config.js
+module.exports = function (api) {
+ api.cache(true);
+ return {
+ presets: ["babel-preset-expo"],
+ plugins: [
+ ['@babel.plugin-transform-react-jsx', {
+ runtime: 'automatic',
+ importSource: 'nativewind',
+ jsxImportSource: 'nativewind',
+ }]
+ ]
+ }
+}
diff --git a/apps/docs/components/Button.docs.tsx b/apps/docs/components/Button.docs.tsx
new file mode 100644
index 0000000..fc0d502
--- /dev/null
+++ b/apps/docs/components/Button.docs.tsx
@@ -0,0 +1,8 @@
+import * as AppButton from '@app/components/Button'
+
+/* --- Documentation overrides? ---------------------------------------------------------------- */
+
+// -i- Optionally wrap and edit these to restyle the component for the docs
+
+export const Button = AppButton.Button
+export const ButtonProps = AppButton.ButtonProps
diff --git a/apps/docs/components/Checkbox.docs.tsx b/apps/docs/components/Checkbox.docs.tsx
new file mode 100644
index 0000000..27c892d
--- /dev/null
+++ b/apps/docs/components/Checkbox.docs.tsx
@@ -0,0 +1,8 @@
+import * as AppCheckbox from '@app/core/forms/Checkbox.styled'
+
+/* --- Documentation overrides? ---------------------------------------------------------------- */
+
+// -i- Optionally wrap and edit these to restyle the component for the docs
+
+export const Checkbox = AppCheckbox.Checkbox
+export const CheckboxProps = AppCheckbox.CheckboxProps
diff --git a/apps/docs/components/Checklist.docs.tsx b/apps/docs/components/Checklist.docs.tsx
new file mode 100644
index 0000000..8aa5c58
--- /dev/null
+++ b/apps/docs/components/Checklist.docs.tsx
@@ -0,0 +1,8 @@
+import * as AppCheckList from '@app/core/forms/CheckList.styled'
+
+/* --- Documentation overrides? ---------------------------------------------------------------- */
+
+// -i- Optionally wrap and edit these to restyle the component for the docs
+
+export const CheckList = AppCheckList.CheckList
+export const CheckListProps = AppCheckList.CheckListProps
diff --git a/apps/docs/components/Hidden.tsx b/apps/docs/components/Hidden.tsx
new file mode 100644
index 0000000..aa9f699
--- /dev/null
+++ b/apps/docs/components/Hidden.tsx
@@ -0,0 +1,16 @@
+import React from 'react'
+
+/* --- ------------------------------------------------------------------------------- */
+
+export const Hidden = ({ children }: { children: React.ReactNode }) => {
+ return (
+
+ {children}
+
+ )
+}
+
+/* --- Aliases --------------------------------------------------------------------------------- */
+
+export const LLMOptimized = Hidden
+export const TitleWrapper = Hidden
diff --git a/apps/docs/components/NumberStepper.docs.tsx b/apps/docs/components/NumberStepper.docs.tsx
new file mode 100644
index 0000000..0fc07c7
--- /dev/null
+++ b/apps/docs/components/NumberStepper.docs.tsx
@@ -0,0 +1,12 @@
+import * as AppNumberStepper from '@app/core/forms/NumberStepper.styled'
+import { styled } from '@app/primitives'
+
+/* --- Documentation overrides? ---------------------------------------------------------------- */
+
+// -i- Optionally wrap and edit these to restyle the component for the docs
+
+export const NumberStepper = styled(AppNumberStepper.NumberStepper, '', {
+ textInputClassName: 'bg-transparent',
+})
+
+export const NumberStepperProps = AppNumberStepper.NumberStepperProps
diff --git a/apps/docs/components/RadioGroup.docs.tsx b/apps/docs/components/RadioGroup.docs.tsx
new file mode 100644
index 0000000..bb42006
--- /dev/null
+++ b/apps/docs/components/RadioGroup.docs.tsx
@@ -0,0 +1,14 @@
+import * as AppRadioGroup from '@app/core/forms/RadioGroup.styled'
+
+/* --- Documentation overrides? ---------------------------------------------------------------- */
+
+// -i- Optionally wrap and edit these to restyle the component for the docs
+
+export const RadioGroup = AppRadioGroup.RadioGroup
+export const RadioGroupProps = AppRadioGroup.RadioGroupProps
+
+export const RadioGroupContext = AppRadioGroup.RadioGroupContext
+export const useRadioGroupContext = AppRadioGroup.useRadioGroupContext
+
+export const RadioButton = AppRadioGroup.RadioButton
+export const RadioButtonProps = AppRadioGroup.RadioButtonProps
diff --git a/apps/docs/components/Select.docs.tsx b/apps/docs/components/Select.docs.tsx
new file mode 100644
index 0000000..d46842b
--- /dev/null
+++ b/apps/docs/components/Select.docs.tsx
@@ -0,0 +1,173 @@
+import type { ReactNode, ElementRef, Dispatch, SetStateAction, LegacyRef } from 'react'
+import { createContext, useContext, useState, useEffect, forwardRef, useRef } from 'react'
+import { useSafeAreaInsets } from 'react-native-safe-area-context'
+import { Platform, StyleSheet, Dimensions } from 'react-native'
+import * as SP from '@green-stack/forms/Select.primitives'
+import * as AppSelect from '@app/core/forms/Select.styled'
+import { cn, styled, View, Text, Pressable, getThemeColor } from '@app/primitives'
+
+/* --- Documentation overrides? ---------------------------------------------------------------- */
+
+// -i- Optionally wrap and edit these to restyle the component for the docs
+
+export const SelectContext = AppSelect.SelectContext
+export const useSelectContext = AppSelect.useSelectContext
+
+export const SelectTrigger = AppSelect.SelectTrigger
+export const SelectTriggerProps = AppSelect.SelectTriggerProps
+
+export const SelectScrollButton = AppSelect.SelectScrollButton
+export const SelectScrollButtonProps = AppSelect.SelectScrollButtonProps
+
+export const SelectContent = styled(AppSelect.SelectContent, 'bg-popover', { nativeID: 'docsTable' }) as typeof AppSelect.SelectContent
+export const SelectContentProps = AppSelect.SelectContentProps
+
+export const SelectLabel = AppSelect.SelectLabel
+export const SelectLabelProps = AppSelect.SelectLabelProps
+
+export const SelectItem = AppSelect.SelectItem
+export const SelectItemProps = AppSelect.SelectItemProps
+
+export const SelectSeparator = AppSelect.SelectSeparator
+export const SelectSeparatorProps = AppSelect.SelectSeparatorProps
+
+export const SelectProps = AppSelect.SelectProps
+
+/* --- Constants ------------------------------------------------------------------------------- */
+
+const isWeb = Platform.OS === 'web'
+const isMobile = ['ios', 'android'].includes(Platform.OS)
+
+/** --- createSelect() ------------------------------------------------------------------------- */
+/** -i- Create a Universal Select where you can pass a Generic type to narrow the string `value` & `onChange()` params */
+export const createSelectComponent = () => Object.assign(forwardRef<
+ ElementRef,
+ AppSelect.SelectProps
+>((rawProps, ref) => {
+ // Props
+ const props = SelectProps.applyDefaults(rawProps)
+ const { placeholder, disabled, hasError, children, onChange, ...restProps } = props
+
+ // State
+ const [value, setValue] = useState(props.value)
+ const [options, setOptions] = useState(props.options)
+
+ // Hooks
+ const insets = useSafeAreaInsets()
+ const contentInsets = {
+ top: insets.top,
+ bottom: insets.bottom,
+ left: 12,
+ right: 12,
+ }
+
+ // Vars
+ const optionsKey = Object.keys(options).join('-')
+ const hasPropOptions = Object.keys(props.options || {}).length > 0
+ const selectValueKey = `${optionsKey}-${!!value}-${!!options?.[value]}`
+
+ // -- Effects --
+
+ useEffect(() => {
+ const isValidOption = value && Object.keys(options || {})?.includes?.(value)
+ if (isValidOption) {
+ onChange(value as T)
+ } else if (!value && !restProps.required) {
+ onChange(undefined as unknown as T)
+ }
+ }, [value])
+
+ useEffect(() => {
+ if (props.value !== value) setValue(props.value)
+ }, [props.value])
+
+ // -- Render --
+
+ return (
+
+ setValue(option!.value!)}
+ disabled={disabled}
+ asChild
+ >
+
+
+
+
+ {isWeb && (
+
+ {options?.[value] || placeholder}
+
+ )}
+
+
+
+
+ {hasPropOptions && (
+
+
+ {!!placeholder && {placeholder}}
+ {Object.entries(props.options).map(([value, label]) => (
+
+ {label}
+
+ ))}
+
+
+ )}
+ {children}
+
+
+
+
+
+
+ )
+}), {
+ displayName: 'Select',
+ Option: SelectItem,
+ Item: SelectItem,
+ Separator: SelectSeparator,
+ Group: SP.SelectGroup,
+ Label: SelectLabel,
+ Content: SelectContent,
+ /** -i- Create a Universal Select where you can pass a Generic type to narrow the string `value` & `onChange()` params */
+ create: createSelectComponent,
+})
+
+/* --- Select ---------------------------------------------------------------------------------- */
+
+export const Select = createSelectComponent()
diff --git a/apps/docs/components/Switch.docs.tsx b/apps/docs/components/Switch.docs.tsx
new file mode 100644
index 0000000..95619c6
--- /dev/null
+++ b/apps/docs/components/Switch.docs.tsx
@@ -0,0 +1,8 @@
+import * as AppSwitch from '@app/core/forms/Switch.styled'
+
+/* --- Documentation overrides? ---------------------------------------------------------------- */
+
+// -i- Optionally wrap and edit these to restyle the component for the docs
+
+export const Switch = AppSwitch.Switch
+export const SwitchProps = AppSwitch.SwitchProps
diff --git a/apps/docs/components/TextArea.docs.tsx b/apps/docs/components/TextArea.docs.tsx
new file mode 100644
index 0000000..8331400
--- /dev/null
+++ b/apps/docs/components/TextArea.docs.tsx
@@ -0,0 +1,9 @@
+import * as AppTextArea from '@app/core/forms/TextArea.styled'
+import { styled } from '@app/primitives'
+
+/* --- Documentation overrides? ---------------------------------------------------------------- */
+
+// -i- Optionally wrap and edit these to restyle the component for the docs
+
+export const TextArea = styled(AppTextArea.TextArea, 'bg-transparent')
+export const TextAreaProps = AppTextArea.TextAreaProps
diff --git a/apps/docs/components/TextInput.docs.tsx b/apps/docs/components/TextInput.docs.tsx
new file mode 100644
index 0000000..c0c3d8f
--- /dev/null
+++ b/apps/docs/components/TextInput.docs.tsx
@@ -0,0 +1,9 @@
+import * as AppTextInput from '@app/core/forms/TextInput.styled'
+import { styled } from '@app/primitives'
+
+/* --- Documentation overrides? ---------------------------------------------------------------- */
+
+// -i- Optionally wrap and edit these to restyle the component for the docs
+
+export const TextInput = styled(AppTextInput.TextInput, 'bg-transparent')
+export const TextInputProps = AppTextInput.TextInputProps
diff --git a/apps/docs/docs.theme.jsx b/apps/docs/docs.theme.jsx
new file mode 100644
index 0000000..931ac53
--- /dev/null
+++ b/apps/docs/docs.theme.jsx
@@ -0,0 +1,202 @@
+import { useRouter } from 'next/router'
+
+/* --- Theme ----------------------------------------------------------------------------------- */
+
+/** @type {import('nextra-theme-docs').DocsThemeConfig} */
+export default {
+ logo: (
+
+ ),
+ },
+ useNextSeoProps() {
+ const { asPath } = useRouter()
+ if (asPath === '/') {
+ return {
+ title: 'FullProduct.dev ⚡️ Universal App Starter',
+ }
+ } else if (asPath.includes('plugins')) {
+ return {
+ titleTemplate: 'FullProduct.dev ⚡️ %s Plugin - Universal App Starter Docs',
+ }
+ }
+ return {
+ titleTemplate: 'FullProduct.dev | %s - Universal App Starter Docs',
+ }
+ }
+}
diff --git a/apps/docs/next-env.d.ts b/apps/docs/next-env.d.ts
new file mode 100644
index 0000000..a4a7b3f
--- /dev/null
+++ b/apps/docs/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs
new file mode 100644
index 0000000..9198b1a
--- /dev/null
+++ b/apps/docs/next.config.mjs
@@ -0,0 +1,14 @@
+import { withExpo } from '@expo/next-adapter'
+import nextra from 'nextra'
+
+const withNextra = nextra({
+ theme: "nextra-theme-docs",
+ themeConfig: "./docs.theme.jsx",
+})
+
+import mainNextConfig from '@app/next/next.config.base.cjs'
+
+/** @type {import('next').NextConfig} */
+const nextConfig = withNextra(withExpo(mainNextConfig))
+
+export default nextConfig
diff --git a/apps/docs/package.json b/apps/docs/package.json
new file mode 100644
index 0000000..5dca3b7
--- /dev/null
+++ b/apps/docs/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@app/docs",
+ "version": "1.0.0",
+ "private": true,
+ "dependencies": {
+ "@mdx-js/loader": "^3.1.0",
+ "@mdx-js/react": "^3.1.0",
+ "@next/mdx": "~15.0.4",
+ "@types/mdx": "^2.0.13",
+ "next": "~15.0.4",
+ "nextra": "^3.3.1",
+ "nextra-theme-docs": "^3.3.1"
+ },
+ "scripts": {
+ "dev": "NEXT_PUBLIC_APP_ENV=docs next -p 4000",
+ "build": "node -v && NEXT_PUBLIC_APP_ENV=docs next build",
+ "start": "NEXT_PUBLIC_APP_ENV=docs next start -p 4000",
+ "env:local": "cp .env.example .env.local",
+ "regenerate:docs": "npm -w @green-stack/core run run:script ../../apps/docs/scripts/regenerate-docs.ts"
+ }
+}
diff --git a/apps/docs/pages/@app-core/components/Button.mdx b/apps/docs/pages/@app-core/components/Button.mdx
new file mode 100644
index 0000000..a398b34
--- /dev/null
+++ b/apps/docs/pages/@app-core/components/Button.mdx
@@ -0,0 +1,199 @@
+import { Button, getDocumentationProps } from '@app/components/Button'
+import { ComponentDocs } from '@app/core/mdx/ComponentDocs'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+import { FileTree, Callout } from 'nextra/components'
+
+
+ ## Button
+
+
+# Button
+
+```typescript copy
+import { Button } from '@app/components/Button'
+```
+
+
+ ### Interactive Preview
+
+
+
+ ### Code Example
+
+
+
+ ## Button Props
+
+
+
+
+
+ ### Props Schema
+
+
+
+Show Props Schema
+
+```typescript copy
+const ButtonProps = z.object({
+ type: z
+ .enum(["primary", "secondary", "outline", "link", "warn", "danger", "info", "success"])
+ .default("primary"),
+ text: z
+ .string()
+ .optional()
+ .example("Press me"),
+ size: z
+ .enum(["sm", "md", "lg"])
+ .default("md"),
+ href: z
+ .string()
+ .url()
+ .optional()
+ .example("https://fullproduct.dev"),
+ iconLeft: z
+ .enum(["AddFilled", "ArrowLeftFilled", "ArrowRightFilled", "CheckFilled", "ChevronDownFilled", "ChevronUpFilled", "RemoveFilled", "UndoFilled"])
+ .optional()
+ .describe("Name of an icon registered in the icon registry"),
+ iconRight: z
+ .enum(["AddFilled", "ArrowLeftFilled", "ArrowRightFilled", "CheckFilled", "ChevronDownFilled", "ChevronUpFilled", "RemoveFilled", "UndoFilled"])
+ .optional()
+ .example("ArrowRightFilled")
+ .describe("Name of an icon registered in the icon registry"),
+ disabled: z
+ .boolean()
+ .optional(),
+ fullWidth: z
+ .boolean()
+ .optional(),
+ className: z
+ .string()
+ .optional(),
+ textClassName: z
+ .string()
+ .optional(),
+ iconSize: z
+ .number()
+ .default(16),
+ hitSlop: z
+ .number()
+ .default(10),
+ target: z
+ .enum(["_blank", "_self", "_parent", "_top"])
+ .default("_self")
+ .example("_blank"),
+ replace: z
+ .boolean()
+ .optional(),
+ push: z
+ .boolean()
+ .optional(),
+})
+```
+> 💡 Could be handy to copy-paste into an AI chat?
+
+
+
+
+
+### Props Type
+
+
+
+Show Props Types
+
+```typescript copy
+{
+ type?: "primary" | "secondary" | "outline" | "link" | "warn" | "danger" | "info" | "success";
+ text?: string;
+ size?: "sm" | "md" | "lg";
+ href?: string | undefined;
+ /** Name of an icon registered in the icon registry */
+ iconLeft?: ("AddFilled" | "ArrowLeftFilled" | "ArrowRightFilled" | "CheckFilled" | "ChevronDownFilled" | "ChevronUpFilled" | "RemoveFilled" | "UndoFilled") | undefined;
+ /** Name of an icon registered in the icon registry */
+ iconRight?: ("AddFilled" | "ArrowLeftFilled" | "ArrowRightFilled" | "CheckFilled" | "ChevronDownFilled" | "ChevronUpFilled" | "RemoveFilled" | "UndoFilled") | undefined;
+ disabled?: boolean;
+ fullWidth?: boolean;
+ className?: string | undefined;
+ textClassName?: string | undefined;
+ iconSize?: number;
+ hitSlop?: number;
+ target?: "_blank" | "_self" | "_parent" | "_top";
+ replace?: boolean | undefined;
+ push?: boolean | undefined;
+}
+```
+> 💡 Could be handy to copy-paste into an AI chat?
+
+
+
+
+
+## Source Code
+
+
+### File Location
+
+You can find the source of the `Button` component in the following location:
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Developer Notes
+
+### `children` vs `text` prop
+
+You can pass either `props.children` or `props.text` to the Button component. If you pass `props.children`, it will render the children as the button content. This supports JSX and other React Components. Whereas `props.text` only supports actual text strings.
+
+If you pass both, `props.children` will be used instead `props.text`.
+
+### `style` prop
+
+Instead of using just `className`, you can also pass a `style` prop. We will combine the styles from `className` and `style` props. This allows you to use inline styles for dynamic styling while still applying CSS classes.
+
+If both classNames and `style` prop influence the same style, the `style` prop will take precedence.
+
+### `onPress` event handlers
+
+Just like react-native's Pressable, our `Button` component supports the `onPress` prop, alongside other event handlers:
+
+- [`onPress()`](https://reactnative.dev/docs/pressable#onpress) - Called when a touch is released.
+- [`onPressIn()`](https://reactnative.dev/docs/pressable#onpressin) - Called when a touch is initiated.
+- [`onPressOut()`](https://reactnative.dev/docs/pressable#onpressout) - Called when a touch is released, after `onPressIn`.
+- [`onHoverIn()`](https://reactnative.dev/docs/pressable#onhoverin) - Called when the pointer enters the button area.
+- [`onHoverOut()`](https://reactnative.dev/docs/pressable#onhoverout) - Called when the pointer leaves the button area.
+- [`onLongPress()`](https://reactnative.dev/docs/pressable#onlongpress) - Called when a long press gesture is detected.
+- [`onBlur()`](https://reactnative.dev/docs/pressable#onblur) - Called when the button loses focus.
+- [`onFocus()`](https://reactnative.dev/docs/pressable#onfocus) - Called when the button gains focus.
+
+Naturally, when `props.disabled` is `true`, all event handlers will be ignored.
+
+If you pass both `href` and `onPress` props, we will try to execute both. Giving preference to the `onPress` handler first.
+
+
+
+## Other
+
+### Disclaimer - Automatic Docgen
+
+
+These dynamic component docs were auto-generated with `npm run regenerate-docs`. You can hook into automatic docgen by exporting `getDocumentationProps` from a component file. You'll want to provide example props from the ComponentProps zod schema, e.g:
+
+
+```tsx /getDocumentationProps/ /documentationProps/ copy filename="Button.tsx"
+/* --- Docs ---------------------- */
+
+export const getDocumentationProps = ButtonProps.documentationProps('Button')
+```
diff --git a/apps/docs/pages/@app-core/components/_meta.ts b/apps/docs/pages/@app-core/components/_meta.ts
new file mode 100644
index 0000000..d46ae39
--- /dev/null
+++ b/apps/docs/pages/@app-core/components/_meta.ts
@@ -0,0 +1,4 @@
+
+export default {
+ 'Button': 'Button',
+}
diff --git a/apps/docs/pages/@app-core/forms/CheckList.mdx b/apps/docs/pages/@app-core/forms/CheckList.mdx
new file mode 100644
index 0000000..7020a09
--- /dev/null
+++ b/apps/docs/pages/@app-core/forms/CheckList.mdx
@@ -0,0 +1,134 @@
+import { CheckList, getDocumentationProps } from '@app/forms/CheckList.styled'
+import { ComponentDocs } from '@app/core/mdx/ComponentDocs'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+import { FileTree, Callout } from 'nextra/components'
+
+
+ ## CheckList
+
+
+# CheckList
+
+```typescript copy
+import { CheckList } from '@app/forms/CheckList.styled'
+```
+
+
+ ### Interactive Preview
+
+
+
+ ### Code Example
+
+
+
+ ## CheckList Props
+
+
+
+
+
+ ### Props Schema
+
+
+
+Show Props Schema
+
+```typescript copy
+const CheckListProps = z.object({
+ disabled: z
+ .boolean()
+ .optional(),
+ hasError: z
+ .boolean()
+ .optional(),
+ className: z
+ .string()
+ .optional(),
+ checkboxClassName: z
+ .string()
+ .optional(),
+ indicatorClassName: z
+ .string()
+ .optional(),
+ labelClassName: z
+ .string()
+ .optional(),
+ hitSlop: z
+ .number()
+ .default(6),
+ options: z
+ .record(z.string(), z.string()),
+ value: z
+ .array(z.string())
+ .default([]),
+})
+```
+> 💡 Could be handy to copy-paste into an AI chat?
+
+
+
+
+
+### Props Type
+
+
+
+Show Props Types
+
+```typescript copy
+{
+ disabled?: boolean;
+ hasError?: boolean;
+ className?: string | undefined;
+ checkboxClassName?: string | undefined;
+ indicatorClassName?: string | undefined;
+ labelClassName?: string | undefined;
+ hitSlop?: number;
+ options: {
+ [x: string]: string;
+ };
+ value?: string[];
+}
+```
+> 💡 Could be handy to copy-paste into an AI chat?
+
+
+
+
+
+## Source Code
+
+
+### File Location
+
+You can find the source of the `CheckList` component in the following location:
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Other
+
+### Disclaimer - Automatic Docgen
+
+
+These dynamic component docs were auto-generated with `npm run regenerate-docs`. You can hook into automatic docgen by exporting `getDocumentationProps` from a component file. You'll want to provide example props from the ComponentProps zod schema, e.g:
+
+
+```tsx /getDocumentationProps/ /documentationProps/ copy filename="CheckList.styled.tsx"
+/* --- Docs ---------------------- */
+
+export const getDocumentationProps = CheckListProps.documentationProps('CheckList')
+```
diff --git a/apps/docs/pages/@app-core/forms/Checkbox.mdx b/apps/docs/pages/@app-core/forms/Checkbox.mdx
new file mode 100644
index 0000000..d8b6615
--- /dev/null
+++ b/apps/docs/pages/@app-core/forms/Checkbox.mdx
@@ -0,0 +1,134 @@
+import { Checkbox, getDocumentationProps } from '@app/forms/Checkbox.styled'
+import { ComponentDocs } from '@app/core/mdx/ComponentDocs'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+import { FileTree, Callout } from 'nextra/components'
+
+
+ ## Checkbox
+
+
+# Checkbox
+
+```typescript copy
+import { Checkbox } from '@app/forms/Checkbox.styled'
+```
+
+
+ ### Interactive Preview
+
+
+
+ ### Code Example
+
+
+
+ ## Checkbox Props
+
+
+
+
+
+ ### Props Schema
+
+
+
+Show Props Schema
+
+```typescript copy
+const CheckboxProps = z.object({
+ checked: z
+ .boolean()
+ .optional(),
+ label: z
+ .string()
+ .optional()
+ .example("Label"),
+ disabled: z
+ .boolean()
+ .optional(),
+ hasError: z
+ .boolean()
+ .optional(),
+ className: z
+ .string()
+ .optional(),
+ checkboxClassName: z
+ .string()
+ .optional(),
+ indicatorClassName: z
+ .string()
+ .optional(),
+ labelClassName: z
+ .string()
+ .optional(),
+ hitSlop: z
+ .number()
+ .default(6),
+})
+```
+> 💡 Could be handy to copy-paste into an AI chat?
+
+
+
+
+
+### Props Type
+
+
+
+Show Props Types
+
+```typescript copy
+{
+ checked?: boolean;
+ label?: string | undefined;
+ disabled?: boolean;
+ hasError?: boolean;
+ className?: string | undefined;
+ checkboxClassName?: string | undefined;
+ indicatorClassName?: string | undefined;
+ labelClassName?: string | undefined;
+ hitSlop?: number;
+}
+```
+> 💡 Could be handy to copy-paste into an AI chat?
+
+
+
+
+
+## Source Code
+
+
+### File Location
+
+You can find the source of the `Checkbox` component in the following location:
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Other
+
+### Disclaimer - Automatic Docgen
+
+
+These dynamic component docs were auto-generated with `npm run regenerate-docs`. You can hook into automatic docgen by exporting `getDocumentationProps` from a component file. You'll want to provide example props from the ComponentProps zod schema, e.g:
+
+
+```tsx /getDocumentationProps/ /documentationProps/ copy filename="Checkbox.styled.tsx"
+/* --- Docs ---------------------- */
+
+export const getDocumentationProps = CheckboxProps.documentationProps('Checkbox')
+```
diff --git a/apps/docs/pages/@app-core/forms/NumberStepper.mdx b/apps/docs/pages/@app-core/forms/NumberStepper.mdx
new file mode 100644
index 0000000..d2254b7
--- /dev/null
+++ b/apps/docs/pages/@app-core/forms/NumberStepper.mdx
@@ -0,0 +1,150 @@
+import { NumberStepper, getDocumentationProps } from '@app/forms/NumberStepper.styled'
+import { ComponentDocs } from '@app/core/mdx/ComponentDocs'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+import { FileTree, Callout } from 'nextra/components'
+
+
+ ## NumberStepper
+
+
+# NumberStepper
+
+```typescript copy
+import { NumberStepper } from '@app/forms/NumberStepper.styled'
+```
+
+
+ ### Interactive Preview
+
+
+
+ ### Code Example
+
+
+
+ ## NumberStepper Props
+
+
+
+
+
+ ### Props Schema
+
+
+
+Show Props Schema
+
+```typescript copy
+const NumberStepperProps = z.object({
+ value: z
+ .number()
+ .optional(),
+ min: z
+ .number()
+ .optional(),
+ max: z
+ .number()
+ .optional(),
+ step: z
+ .number()
+ .default(1),
+ placeholder: z
+ .string()
+ .optional()
+ .example("Enter number..."),
+ disabled: z
+ .boolean()
+ .optional(),
+ readOnly: z
+ .boolean()
+ .optional(),
+ hasError: z
+ .boolean()
+ .optional(),
+ className: z
+ .string()
+ .optional(),
+ pressableClassName: z
+ .string()
+ .optional(),
+ textInputClassName: z
+ .string()
+ .optional(),
+ placeholderClassName: z
+ .string()
+ .optional(),
+ placeholderTextColor: z
+ .string()
+ .optional(),
+})
+```
+> 💡 Could be handy to copy-paste into an AI chat?
+
+
+
+
+
+### Props Type
+
+
+
+Show Props Types
+
+```typescript copy
+{
+ value?: number;
+ min?: number;
+ max?: number | undefined;
+ step?: number;
+ placeholder?: string | undefined;
+ disabled?: boolean;
+ readOnly?: boolean;
+ hasError?: boolean;
+ className?: string | undefined;
+ pressableClassName?: string | undefined;
+ textInputClassName?: string | undefined;
+ placeholderClassName?: string | undefined;
+ placeholderTextColor?: string | undefined;
+}
+```
+> 💡 Could be handy to copy-paste into an AI chat?
+
+
+
+
+
+## Source Code
+
+
+### File Location
+
+You can find the source of the `NumberStepper` component in the following location:
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Other
+
+### Disclaimer - Automatic Docgen
+
+
+These dynamic component docs were auto-generated with `npm run regenerate-docs`. You can hook into automatic docgen by exporting `getDocumentationProps` from a component file. You'll want to provide example props from the ComponentProps zod schema, e.g:
+
+
+```tsx /getDocumentationProps/ /documentationProps/ copy filename="NumberStepper.styled.tsx"
+/* --- Docs ---------------------- */
+
+export const getDocumentationProps = NumberStepperProps.documentationProps('NumberStepper')
+```
diff --git a/apps/docs/pages/@app-core/forms/RadioGroup.mdx b/apps/docs/pages/@app-core/forms/RadioGroup.mdx
new file mode 100644
index 0000000..1193d69
--- /dev/null
+++ b/apps/docs/pages/@app-core/forms/RadioGroup.mdx
@@ -0,0 +1,134 @@
+import { RadioGroup, getDocumentationProps } from '@app/forms/RadioGroup.styled'
+import { ComponentDocs } from '@app/core/mdx/ComponentDocs'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+import { FileTree, Callout } from 'nextra/components'
+
+
+ ## RadioGroup
+
+
+# RadioGroup
+
+```typescript copy
+import { RadioGroup } from '@app/forms/RadioGroup.styled'
+```
+
+
+ ### Interactive Preview
+
+
+
+ ### Code Example
+
+
+
+ ## RadioGroup Props
+
+
+
+
+
+ ### Props Schema
+
+
+
+Show Props Schema
+
+```typescript copy
+const RadioGroupProps = z.object({
+ value: z
+ .string()
+ .optional(),
+ disabled: z
+ .boolean()
+ .optional(),
+ hasError: z
+ .boolean()
+ .optional(),
+ className: z
+ .string()
+ .optional(),
+ radioButtonClassName: z
+ .string()
+ .optional(),
+ indicatorClassName: z
+ .string()
+ .optional(),
+ labelClassName: z
+ .string()
+ .optional(),
+ hitSlop: z
+ .number()
+ .default(6),
+ options: z
+ .record(z.string(), z.string()),
+})
+```
+> 💡 Could be handy to copy-paste into an AI chat?
+
+
+
+
+
+### Props Type
+
+
+
+Show Props Types
+
+```typescript copy
+{
+ value?: string;
+ disabled?: boolean;
+ hasError?: boolean;
+ className?: string | undefined;
+ radioButtonClassName?: string | undefined;
+ indicatorClassName?: string | undefined;
+ labelClassName?: string | undefined;
+ hitSlop?: number;
+ options: {
+ [x: string]: string;
+ };
+}
+```
+> 💡 Could be handy to copy-paste into an AI chat?
+
+
+
+
+
+## Source Code
+
+
+### File Location
+
+You can find the source of the `RadioGroup` component in the following location:
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Other
+
+### Disclaimer - Automatic Docgen
+
+
+These dynamic component docs were auto-generated with `npm run regenerate-docs`. You can hook into automatic docgen by exporting `getDocumentationProps` from a component file. You'll want to provide example props from the ComponentProps zod schema, e.g:
+
+
+```tsx /getDocumentationProps/ /documentationProps/ copy filename="RadioGroup.styled.tsx"
+/* --- Docs ---------------------- */
+
+export const getDocumentationProps = RadioGroupProps.documentationProps('RadioGroup')
+```
diff --git a/apps/docs/pages/@app-core/forms/Select.mdx b/apps/docs/pages/@app-core/forms/Select.mdx
new file mode 100644
index 0000000..b87f4f4
--- /dev/null
+++ b/apps/docs/pages/@app-core/forms/Select.mdx
@@ -0,0 +1,65 @@
+import { Select, getDocumentationProps } from '@app/forms/Select.styled'
+import { ComponentDocs } from '@app/core/mdx/ComponentDocs'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+import { FileTree, Callout } from 'nextra/components'
+
+
+ ## Select
+
+
+# Select
+
+```typescript copy
+import { Select } from '@app/forms/Select.styled'
+```
+
+
+ ### Interactive Preview
+
+
+
+ ### Code Example
+
+
+
+ ## Select Props
+
+
+
+
+
+## Source Code
+
+
+### File Location
+
+You can find the source of the `Select` component in the following location:
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Other
+
+### Disclaimer - Automatic Docgen
+
+
+These dynamic component docs were auto-generated with `npm run regenerate-docs`. You can hook into automatic docgen by exporting `getDocumentationProps` from a component file. You'll want to provide example props from the ComponentProps zod schema, e.g:
+
+
+```tsx /getDocumentationProps/ /documentationProps/ copy filename="Select.styled.tsx"
+/* --- Docs ---------------------- */
+
+export const getDocumentationProps = SelectProps.documentationProps('Select')
+```
diff --git a/apps/docs/pages/@app-core/forms/Switch.mdx b/apps/docs/pages/@app-core/forms/Switch.mdx
new file mode 100644
index 0000000..044a07d
--- /dev/null
+++ b/apps/docs/pages/@app-core/forms/Switch.mdx
@@ -0,0 +1,65 @@
+import { Switch, getDocumentationProps } from '@app/forms/Switch.styled'
+import { ComponentDocs } from '@app/core/mdx/ComponentDocs'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+import { FileTree, Callout } from 'nextra/components'
+
+
+ ## Switch
+
+
+# Switch
+
+```typescript copy
+import { Switch } from '@app/forms/Switch.styled'
+```
+
+
+ ### Interactive Preview
+
+
+
+ ### Code Example
+
+
+
+ ## Switch Props
+
+
+
+
+
+## Source Code
+
+
+### File Location
+
+You can find the source of the `Switch` component in the following location:
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Other
+
+### Disclaimer - Automatic Docgen
+
+
+These dynamic component docs were auto-generated with `npm run regenerate-docs`. You can hook into automatic docgen by exporting `getDocumentationProps` from a component file. You'll want to provide example props from the ComponentProps zod schema, e.g:
+
+
+```tsx /getDocumentationProps/ /documentationProps/ copy filename="Switch.styled.tsx"
+/* --- Docs ---------------------- */
+
+export const getDocumentationProps = SwitchProps.documentationProps('Switch')
+```
diff --git a/apps/docs/pages/@app-core/forms/TextArea.mdx b/apps/docs/pages/@app-core/forms/TextArea.mdx
new file mode 100644
index 0000000..febc5ae
--- /dev/null
+++ b/apps/docs/pages/@app-core/forms/TextArea.mdx
@@ -0,0 +1,152 @@
+import { TextArea, getDocumentationProps } from '@app/forms/TextArea.styled'
+import { ComponentDocs } from '@app/core/mdx/ComponentDocs'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+import { FileTree, Callout } from 'nextra/components'
+
+
+ ## TextArea
+
+
+# TextArea
+
+```typescript copy
+import { TextArea } from '@app/forms/TextArea.styled'
+```
+
+
+ ### Interactive Preview
+
+
+
+ ### Code Example
+
+
+
+ ## TextArea Props
+
+
+
+
+
+ ### Props Schema
+
+
+
+Show Props Schema
+
+```typescript copy
+const TextAreaProps = z.object({
+ value: z
+ .string()
+ .optional(),
+ placeholder: z
+ .string()
+ .optional()
+ .example("Start typing..."),
+ className: z
+ .string()
+ .optional(),
+ placeholderClassName: z
+ .string()
+ .optional(),
+ placeholderTextColor: z
+ .string()
+ .default("hsl(var(--muted))"),
+ hasError: z
+ .boolean()
+ .optional(),
+ readOnly: z
+ .boolean()
+ .optional(),
+ disabled: z
+ .boolean()
+ .optional(),
+ multiline: z
+ .boolean()
+ .default(true)
+ .describe("See `numberOfLines`. Also disables some `textAlign` props."),
+ numberOfLines: z
+ .number()
+ .optional(),
+ textAlign: z
+ .enum(["left", "center", "right"])
+ .default("left")
+ .describe("Might not work if multiline"),
+ textAlignVertical: z
+ .enum(["top", "center", "bottom"])
+ .default("top")
+ .describe("Might not work if multiline"),
+})
+```
+> 💡 Could be handy to copy-paste into an AI chat?
+
+
+
+
+
+### Props Type
+
+
+
+Show Props Types
+
+```typescript copy
+{
+ value?: string | undefined;
+ placeholder?: string | undefined;
+ className?: string | undefined;
+ placeholderClassName?: string | undefined;
+ placeholderTextColor?: string;
+ hasError?: boolean;
+ readOnly?: boolean;
+ disabled?: boolean;
+ /** See `numberOfLines`. Also disables some `textAlign` props. */
+ multiline?: boolean;
+ numberOfLines?: number | undefined;
+ /** Might not work if multiline */
+ textAlign?: "left" | "center" | "right";
+ /** Might not work if multiline */
+ textAlignVertical?: "top" | "center" | "bottom";
+}
+```
+> 💡 Could be handy to copy-paste into an AI chat?
+
+
+
+
+
+## Source Code
+
+
+### File Location
+
+You can find the source of the `TextArea` component in the following location:
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Other
+
+### Disclaimer - Automatic Docgen
+
+
+These dynamic component docs were auto-generated with `npm run regenerate-docs`. You can hook into automatic docgen by exporting `getDocumentationProps` from a component file. You'll want to provide example props from the ComponentProps zod schema, e.g:
+
+
+```tsx /getDocumentationProps/ /documentationProps/ copy filename="TextArea.styled.tsx"
+/* --- Docs ---------------------- */
+
+export const getDocumentationProps = TextAreaProps.documentationProps('TextArea')
+```
diff --git a/apps/docs/pages/@app-core/forms/TextInput.mdx b/apps/docs/pages/@app-core/forms/TextInput.mdx
new file mode 100644
index 0000000..92e4c85
--- /dev/null
+++ b/apps/docs/pages/@app-core/forms/TextInput.mdx
@@ -0,0 +1,130 @@
+import { TextInput, getDocumentationProps } from '@app/forms/TextInput.styled'
+import { ComponentDocs } from '@app/core/mdx/ComponentDocs'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+import { FileTree, Callout } from 'nextra/components'
+
+
+ ## TextInput
+
+
+# TextInput
+
+```typescript copy
+import { TextInput } from '@app/forms/TextInput.styled'
+```
+
+
+ ### Interactive Preview
+
+
+
+ ### Code Example
+
+
+
+ ## TextInput Props
+
+
+
+
+
+ ### Props Schema
+
+
+
+Show Props Schema
+
+```typescript copy
+const TextInputProps = z.object({
+ value: z
+ .string()
+ .optional(),
+ placeholder: z
+ .string()
+ .optional()
+ .example("Start typing..."),
+ className: z
+ .string()
+ .optional(),
+ placeholderClassName: z
+ .string()
+ .optional(),
+ placeholderTextColor: z
+ .string()
+ .default("hsl(var(--muted))"),
+ hasError: z
+ .boolean()
+ .optional(),
+ readOnly: z
+ .boolean()
+ .optional(),
+ disabled: z
+ .boolean()
+ .optional(),
+})
+```
+> 💡 Could be handy to copy-paste into an AI chat?
+
+
+
+
+
+### Props Type
+
+
+
+Show Props Types
+
+```typescript copy
+{
+ value?: string | undefined;
+ placeholder?: string | undefined;
+ className?: string | undefined;
+ placeholderClassName?: string | undefined;
+ placeholderTextColor?: string;
+ hasError?: boolean;
+ readOnly?: boolean;
+ disabled?: boolean;
+}
+```
+> 💡 Could be handy to copy-paste into an AI chat?
+
+
+
+
+
+## Source Code
+
+
+### File Location
+
+You can find the source of the `TextInput` component in the following location:
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Other
+
+### Disclaimer - Automatic Docgen
+
+
+These dynamic component docs were auto-generated with `npm run regenerate-docs`. You can hook into automatic docgen by exporting `getDocumentationProps` from a component file. You'll want to provide example props from the ComponentProps zod schema, e.g:
+
+
+```tsx /getDocumentationProps/ /documentationProps/ copy filename="TextInput.styled.tsx"
+/* --- Docs ---------------------- */
+
+export const getDocumentationProps = TextInputProps.documentationProps('TextInput')
+```
diff --git a/apps/docs/pages/@app-core/forms/_meta.ts b/apps/docs/pages/@app-core/forms/_meta.ts
new file mode 100644
index 0000000..c001f70
--- /dev/null
+++ b/apps/docs/pages/@app-core/forms/_meta.ts
@@ -0,0 +1,11 @@
+
+export default {
+ 'TextInput': 'TextInput',
+ 'TextArea': 'TextArea',
+ 'Switch': 'Switch',
+ 'Select': 'Select',
+ 'RadioGroup': 'RadioGroup',
+ 'NumberStepper': 'NumberStepper',
+ 'Checkbox': 'Checkbox',
+ 'CheckList': 'CheckList',
+}
diff --git a/apps/docs/pages/@app-core/resolvers/_meta.ts b/apps/docs/pages/@app-core/resolvers/_meta.ts
new file mode 100644
index 0000000..6cd90c2
--- /dev/null
+++ b/apps/docs/pages/@app-core/resolvers/_meta.ts
@@ -0,0 +1,4 @@
+
+export default {
+ 'healthCheck': 'healthCheck',
+}
diff --git a/apps/docs/pages/@app-core/resolvers/healthCheck.mdx b/apps/docs/pages/@app-core/resolvers/healthCheck.mdx
new file mode 100644
index 0000000..3faa242
--- /dev/null
+++ b/apps/docs/pages/@app-core/resolvers/healthCheck.mdx
@@ -0,0 +1,434 @@
+import { FileTree, Callout } from 'nextra/components'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+import { View, Image } from '@app/primitives'
+
+
+ ## `healthCheck` - API
+
+
+# healthCheck() - Resolver
+
+
+
+
+
+
+
+
+
+
+
+
+
+`healthCheck()` is a query resolver that allows you to get data from:
+
+- [Async Functions](#server-usage) during other resolver logic / GraphQL / API calls server-side
+- [GraphQL](#graphql-query) - As a GraphQL query
+- [API route](#nextjs-api-route) (GET)
+- [Clientside Hooks](#client-usage) for calling the API with `react-query` from Web / Mobile
+
+
+
+## Resolver Config
+
+Input / output types, defaults, schemas and general config for the `healthCheck()` resolver are defined in its DataBridge file. Importable from:
+
+```typescript copy
+import { healthCheckBridge } from '@app/core/resolvers/healthCheck.bridge'
+```
+
+
+
+### Input Shape
+
+You can find the schema used to validate the input arguments for the `healthCheck()` resolver in the bridge config:
+
+```typescript copy
+const HealthCheckInput = healthCheckBridge.inputSchema
+```
+
+
+Show Input Schema
+
+```typescript copy
+const HealthCheckInput = z.object({
+ echo: z
+ .string()
+ .default("Hello World"),
+ verbose: z
+ .boolean()
+ .optional(),
+})
+```
+> 💡 Could be handy to copy-paste into an AI chat?
+
+
+
+
+If needed, you can extract the TypeScript type from the schema using `z.input()`, e.g.:
+
+```typescript copy
+type HealthCheckInput = z.input
+```
+
+
+
+Show Input Type
+
+```typescript copy
+{
+ echo?: string;
+ verbose?: boolean;
+}
+```
+> 💡 Could be handy to copy-paste into an AI chat?
+
+
+
+
+### Output Shape
+
+You can find the schema used to provide output defaults for the `healthCheck()` resolver in the bridge config too:
+
+```typescript copy
+const HealthCheckOutput = healthCheckBridge.outputSchema
+```
+
+
+Show Output Schema
+
+```typescript copy
+const HealthCheckOutput = z.object({
+ echo: z
+ .string()
+ .optional(),
+ status: z
+ .literal("OK"),
+ alive: z
+ .boolean(),
+ kicking: z
+ .boolean(),
+ now: z
+ .string(),
+ aliveTime: z
+ .number(),
+ aliveSince: z
+ .date(),
+ serverTimezone: z
+ .string(),
+ requestHost: z
+ .string()
+ .optional(),
+ requestProtocol: z
+ .string()
+ .optional(),
+ requestURL: z
+ .string()
+ .optional(),
+ baseURL: z
+ .string()
+ .optional(),
+ backendURL: z
+ .string()
+ .optional(),
+ apiURL: z
+ .string()
+ .optional(),
+ graphURL: z
+ .string()
+ .optional(),
+ port: z
+ .number()
+ .int()
+ .nullable(),
+ debugPort: z
+ .number()
+ .int()
+ .nullable(),
+ nodeVersion: z
+ .string()
+ .optional(),
+ v8Version: z
+ .string()
+ .optional(),
+ systemArch: z
+ .string()
+ .optional(),
+ systemPlatform: z
+ .string()
+ .optional(),
+ systemRelease: z
+ .string()
+ .optional(),
+ systemFreeMemory: z
+ .number()
+ .optional(),
+ systemTotalMemory: z
+ .number()
+ .optional(),
+ systemLoadAverage: z
+ .array(z.number())
+ .optional(),
+ context: z
+ .record(z.string(), z.unknown())
+ .nullish(),
+})
+```
+> 💡 Could be handy to copy-paste into an AI chat?
+
+
+
+
+Here too, you can extract the TypeScript type from the schema using `z.output()`, e.g.:
+
+```typescript copy
+type HealthCheckOutput = z.output
+```
+
+
+Show Output Type
+
+```typescript copy
+{
+ echo?: string | undefined;
+ status: "OK";
+ alive: boolean;
+ kicking: boolean;
+ now: string;
+ aliveTime: number;
+ aliveSince: Date;
+ serverTimezone: string;
+ requestHost?: string | undefined;
+ requestProtocol?: string | undefined;
+ requestURL?: string | undefined;
+ baseURL?: string | undefined;
+ backendURL?: string | undefined;
+ apiURL?: string | undefined;
+ graphURL?: string | undefined;
+ port: number | null;
+ debugPort: number | null;
+ nodeVersion?: string | undefined;
+ v8Version?: string | undefined;
+ systemArch?: string | undefined;
+ systemPlatform?: string | undefined;
+ systemRelease?: string | undefined;
+ systemFreeMemory?: number | undefined;
+ systemTotalMemory?: number | undefined;
+ systemLoadAverage?: number[] | undefined;
+ context?: ({
+ [x: string]: unknown;
+ } | null) | undefined;
+}
+```
+> 💡 Could be handy to copy-paste into an AI chat?
+
+
+
+
+## Server Usage
+
+
+
+### `healthCheck()` function
+
+```typescript copy
+import { healthCheck } from '@app/core/resolvers/healthCheck.resolver'
+```
+
+```typescript copy
+// ... Later, in resolver or script logic ...
+const output = await healthCheck({ ...inputArgs })
+// ?^ HealthCheckOutput
+```
+
+Note that using resolvers like `healthCheck()` as async functions is only available server-side, and might cause issues if imported into the client bundle. For client-side usage, use any of the other options below.
+
+
+
+## GraphQL Query
+
+
+
+### `healthCheckFetcher()`
+
+```typescript copy
+import { healthCheckFetcher } from '@app/core/resolvers/healthCheck.query'
+```
+
+`healthCheckFetcher()` is a universal GraphQL query fetcher function.
+It wil query the `healthCheck` resolver for you as a GraphQL query:
+
+```typescript copy
+const response = await healthCheckFetcher({ healthCheckArgs: { ...inputArgs } })
+// ?^ { healthCheck: HealthCheckOutput }
+```
+
+Just make sure the `healthCheckArgs` input matches the [`HealthCheckInput`](#input-shape) schema.
+
+If you prefer, you can also use the following GraphQL snippet in your own GraphQL fetching logic:
+
+
+
+### GraphQL Query Snippet
+
+```graphql copy
+query healthCheck($healthCheckArgs: HealthCheckInput!) {
+ healthCheck(args: $healthCheckArgs) {
+ echo
+ status
+ alive
+ kicking
+ now
+ aliveTime
+ aliveSince
+ serverTimezone
+ requestHost
+ requestProtocol
+ requestURL
+ baseURL
+ backendURL
+ apiURL
+ graphURL
+ port
+ debugPort
+ nodeVersion
+ v8Version
+ systemArch
+ systemPlatform
+ systemRelease
+ systemFreeMemory
+ systemTotalMemory
+ systemLoadAverage
+ context {
+ zodType
+ baseType
+ }
+ }
+}
+```
+
+
+
+### Custom Query
+
+Using a custom query, you can omit certain fields you don't need and request only what's necessary.
+
+If you do, we suggest using `graphqlQuery()`, as it works seamlessly on the server, browser and mobile app:
+
+```typescript copy
+import { graphql } from '@app/core/graphql/graphql'
+import { graphqlQuery } from '@app/core/graphql/graphqlQuery'
+
+const query = graphql(`
+ query healthCheck($healthCheckArgs: HealthCheckInput!) {
+ healthCheck(args: $healthCheckArgs) {
+ // -i- ... type hints for the fields you need ... -i-
+ }
+ }
+`)
+
+const response = await graphqlQuery(query, { healthCheckArgs: { ...inputArgs } })
+// ?^ { healthCheck: HealthCheckOutput }
+```
+
+Just make sure the `healthCheckArgs` input matches the [`HealthCheckInput`](#input-shape) schema.
+
+
+
+## Next.js API Route
+
+
+
+### `GET` requests
+
+```shell copy
+GET /api/health?...
+```
+
+Provide query parameters as needed (e.g. `?someArg=123`).
+
+Make sure the params / query input match the [`HealthCheckInput`](#input-shape) schema.
+
+
+
+
+
+## Client Usage
+
+
+
+### Custom `react-query` hook
+
+> e.g. In the `healthCheck.query.ts` file:
+
+```typescript copy
+import { useQuery, UseQueryOptions, QueryKey } from '@tanstack/react-query'
+```
+
+```typescript copy
+
+export const useHealthCheckQuery = (
+ input: HealthCheckQueryInput,
+ options?: Omit, 'queryFn' | 'queryKey'> & {
+ queryKey?: QueryKey,
+ },
+) => {
+ return useQuery({
+ queryKey: ['healthCheckFetcher', input],
+ queryFn: (context) => healthCheckFetcher(input),
+ ...options,
+ })
+}
+```
+
+Be sure to check the [`useQuery`](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery) docs for all the available options you might want to prefill or abstract.
+
+
+
+### Usage in React
+
+```typescript copy
+import { useHealthCheckQuery } from '@app/core/resolvers/healthCheck.query'
+```
+
+```typescript copy
+const { data, error, isLoading } = useHealthCheckQuery({ healthCheckArgs: /* ... */ }, {
+ // ... any additional options ...
+})
+```
+
+Be sure to check the [`useQuery`](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery) docs for all the available options.
+
+
+
+## Developer Notes
+
+### Why this resolver?
+
+We included this resolver in the starterkit as a reference implementation for a simple API resolver.
+
+While it can be used to verify that the application is running and responsive, its main goals for inclusion are to demonstrate:
+
+- How to create a basic API resolver.
+- What our way of working of creating resolvers gets you. (typesafety, async function, REST API, GraphQL API, docs)
+- Automatic docgen for API resolvers. (this entire docs page)
+
+### Adding your own resolvers
+
+If you want to quickly replicate this way of working for setting up APIs with all the side-effects listed above, you can use our resolver generator:
+
+```bash copy
+npm run add:resolver
+```
+
+
+
+## Other
+
+### Disclaimer - Automatic Docgen
+
+
+These dynamic API docs were auto-generated with `npm run regenerate-docs`. This happens from `.bridge.ts` files in any `/resolvers/` folder.
+
+You can opt-out of this by adding `export const optOut = true` somewhere in the file.
+
diff --git a/apps/docs/pages/@app-core/schemas/HealthCheckInput.mdx b/apps/docs/pages/@app-core/schemas/HealthCheckInput.mdx
new file mode 100644
index 0000000..9636901
--- /dev/null
+++ b/apps/docs/pages/@app-core/schemas/HealthCheckInput.mdx
@@ -0,0 +1,156 @@
+import { FileTree, Callout } from 'nextra/components'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+import { View, Image } from '@app/primitives'
+
+
+ ## HealthCheckInput
+
+
+# HealthCheckInput
+
+```typescript copy
+import { HealthCheckInput } from '@app/core/schemas/HealthCheckInput'
+```
+
+### Location
+
+
+
+
+
+
+
+
+
+
+
+### Zod Schema
+
+What the schema would look like when defined with `z.object()` in Zod V3:
+
+```typescript copy
+const HealthCheckInput = z.object({
+ echo: z
+ .string()
+ .default("Hello World"),
+ verbose: z
+ .boolean()
+ .optional(),
+})
+```
+
+> (💡 Could be handy to copy-paste this schema info into an AI chat assistant)
+
+
+### Type Definition
+
+You can extract the TypeScript type from the schema using `z.input()`, `z.output()` or `z.infer()` methods. e.g.:
+
+```typescript copy
+type HealthCheckInput = z.input
+```
+
+What the resulting TypeScript type would look like:
+
+```typescript copy
+{
+ echo?: string;
+ verbose?: boolean;
+}
+```
+
+> (💡 Could be handy to copy-paste this type info into an AI chat assistant)
+
+
+### Usage - Validation
+
+To validate data against this schema, you have a few options:
+
+```typescript copy
+// Throws if invalid
+const healthCheckInput = HealthCheckInput.parse(data)
+
+// Returns { success: boolean, data?: T, error?: ZodError }
+const healthCheckInput = HealthCheckInput.safeParse(data)
+
+```
+
+> This might be useful for parsing API input data or validating form data before submission.
+
+> You can also directly integrate this schema with form state managers like our own:
+
+
+
+### Usage - Form State
+
+```typescript copy
+import { useFormState } from '@green-stack/forms/useFormState'
+
+const formState = useFormState(HealthCheckInput, {
+ initialValues: { /* ... */ }, // Provide initial values?
+ validateOnMount: true, // Validate on component mount?
+})
+
+```
+
+Learn more about using schemas for form state in our [Form Management Docs](/form-management).
+
+
+
+### Usage - Component Props / Docs
+
+Another potential use case for the 'HealthCheckInput' schema is to type component props, provide default values and generate documentation for that component:
+
+```typescript copy
+export const HealthCheckInputComponentProps = HealthCheckInput.extend({
+ // Add any additional props here
+})
+
+export type HealthCheckInputComponentProps = z.input
+
+/* --- --------------- */
+
+export const HealthCheckInputComponent = (rawProps: HealthCheckInputComponentProps) => {
+
+ // Extract the props and apply defaults + infer resulting type
+ const props = ComponentProps.applyDefaults(rawProps)
+
+ // ... rest of the component logic ...
+
+}
+
+/* --- Documentation --------------- */
+
+export const documentationProps = HealthCheckInputComponentProps.documentationProps('HealthCheckInputComponent')
+
+```
+
+
+
+## Developer Notes
+
+### Why this schema?
+
+We included this schema in the starterkit as a reference guide for what schema docs look like.
+
+It's also reused in the [`healthCheck()`](/@app-core/resolvers/healthCheck) resolver, which has it's own auto-generated docs example.
+
+Learn more about how zod schemas work as the [Single Source of Truth](/single-sources-of-truth) for APIs, forms, GraphQL and other automations we consider the "right abstractions".
+
+### Adding your own schemas
+
+If you want to quickly replicate this way of working for setting up schemas with all the side-effects listed above, you can use our schema generator:
+
+```bash copy
+npm run add:schema
+```
+
+
+
+## Other
+
+### Disclaimer - Automatic Docgen
+
+
+These dynamic schema docs were auto-generated with `npm run regenerate-docs`. This happens automatically for schema files in any `\schemas\` folder. You can opt-out of this by adding `// export const optOut = true` somewhere in the file.
+
diff --git a/apps/docs/pages/@app-core/schemas/HealthCheckOutput.mdx b/apps/docs/pages/@app-core/schemas/HealthCheckOutput.mdx
new file mode 100644
index 0000000..b40e90f
--- /dev/null
+++ b/apps/docs/pages/@app-core/schemas/HealthCheckOutput.mdx
@@ -0,0 +1,249 @@
+import { FileTree, Callout } from 'nextra/components'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+import { View, Image } from '@app/primitives'
+
+
+ ## HealthCheckOutput
+
+
+# HealthCheckOutput
+
+```typescript copy
+import { HealthCheckOutput } from '@app/core/schemas/HealthCheckOutput'
+```
+
+### Location
+
+
+
+
+
+
+
+
+
+
+
+### Zod Schema
+
+What the schema would look like when defined with `z.object()` in Zod V3:
+
+```typescript copy
+const HealthCheckOutput = z.object({
+ echo: z
+ .string()
+ .optional(),
+ status: z
+ .literal("OK"),
+ alive: z
+ .boolean(),
+ kicking: z
+ .boolean(),
+ now: z
+ .string(),
+ aliveTime: z
+ .number(),
+ aliveSince: z
+ .date(),
+ serverTimezone: z
+ .string(),
+ requestHost: z
+ .string()
+ .optional(),
+ requestProtocol: z
+ .string()
+ .optional(),
+ requestURL: z
+ .string()
+ .optional(),
+ baseURL: z
+ .string()
+ .optional(),
+ backendURL: z
+ .string()
+ .optional(),
+ apiURL: z
+ .string()
+ .optional(),
+ graphURL: z
+ .string()
+ .optional(),
+ port: z
+ .number()
+ .int()
+ .nullable(),
+ debugPort: z
+ .number()
+ .int()
+ .nullable(),
+ nodeVersion: z
+ .string()
+ .optional(),
+ v8Version: z
+ .string()
+ .optional(),
+ systemArch: z
+ .string()
+ .optional(),
+ systemPlatform: z
+ .string()
+ .optional(),
+ systemRelease: z
+ .string()
+ .optional(),
+ systemFreeMemory: z
+ .number()
+ .optional(),
+ systemTotalMemory: z
+ .number()
+ .optional(),
+ systemLoadAverage: z
+ .array(z.number())
+ .optional(),
+ context: z
+ .record(z.string(), z.unknown())
+ .nullish(),
+})
+```
+
+> (💡 Could be handy to copy-paste this schema info into an AI chat assistant)
+
+
+### Type Definition
+
+You can extract the TypeScript type from the schema using `z.input()`, `z.output()` or `z.infer()` methods. e.g.:
+
+```typescript copy
+type HealthCheckOutput = z.input
+```
+
+What the resulting TypeScript type would look like:
+
+```typescript copy
+{
+ echo?: string | undefined;
+ status: "OK";
+ alive: boolean;
+ kicking: boolean;
+ now: string;
+ aliveTime: number;
+ aliveSince: Date;
+ serverTimezone: string;
+ requestHost?: string | undefined;
+ requestProtocol?: string | undefined;
+ requestURL?: string | undefined;
+ baseURL?: string | undefined;
+ backendURL?: string | undefined;
+ apiURL?: string | undefined;
+ graphURL?: string | undefined;
+ port: number | null;
+ debugPort: number | null;
+ nodeVersion?: string | undefined;
+ v8Version?: string | undefined;
+ systemArch?: string | undefined;
+ systemPlatform?: string | undefined;
+ systemRelease?: string | undefined;
+ systemFreeMemory?: number | undefined;
+ systemTotalMemory?: number | undefined;
+ systemLoadAverage?: number[] | undefined;
+ context?: ({
+ [x: string]: unknown;
+ } | null) | undefined;
+}
+```
+
+> (💡 Could be handy to copy-paste this type info into an AI chat assistant)
+
+
+### Usage - Validation
+
+To validate data against this schema, you have a few options:
+
+```typescript copy
+// Throws if invalid
+const healthCheckOutput = HealthCheckOutput.parse(data)
+
+// Returns { success: boolean, data?: T, error?: ZodError }
+const healthCheckOutput = HealthCheckOutput.safeParse(data)
+
+```
+
+> This might be useful for parsing API input data or validating form data before submission.
+
+> You can also directly integrate this schema with form state managers like our own:
+
+
+
+### Usage - Form State
+
+```typescript copy
+import { useFormState } from '@green-stack/forms/useFormState'
+
+const formState = useFormState(HealthCheckOutput, {
+ initialValues: { /* ... */ }, // Provide initial values?
+ validateOnMount: true, // Validate on component mount?
+})
+
+```
+
+Learn more about using schemas for form state in our [Form Management Docs](/form-management).
+
+
+
+### Usage - Component Props / Docs
+
+Another potential use case for the 'HealthCheckOutput' schema is to type component props, provide default values and generate documentation for that component:
+
+```typescript copy
+export const HealthCheckOutputComponentProps = HealthCheckOutput.extend({
+ // Add any additional props here
+})
+
+export type HealthCheckOutputComponentProps = z.input
+
+/* --- --------------- */
+
+export const HealthCheckOutputComponent = (rawProps: HealthCheckOutputComponentProps) => {
+
+ // Extract the props and apply defaults + infer resulting type
+ const props = ComponentProps.applyDefaults(rawProps)
+
+ // ... rest of the component logic ...
+
+}
+
+/* --- Documentation --------------- */
+
+export const documentationProps = HealthCheckOutputComponentProps.documentationProps('HealthCheckOutputComponent')
+
+```
+
+
+
+## Developer Notes
+
+### Why this schema?
+
+We included this schema in the starterkit as a reference guide for what schema docs look like.
+
+It's also reused in the [`healthCheck()`](/@app-core/resolvers/healthCheck) resolver, which has it's own auto-generated docs example.
+
+Learn more about how zod schemas work as the [Single Source of Truth](/single-sources-of-truth) for APIs, forms, GraphQL and other automations we consider the "right abstractions".
+
+### Adding your own schemas
+
+If you want to quickly replicate this way of working for setting up schemas with all the side-effects listed above, you can use our schema generator:
+
+```bash copy
+npm run add:schema
+```
+
+
+
+## Other
+
+### Disclaimer - Automatic Docgen
+
+
+These dynamic schema docs were auto-generated with `npm run regenerate-docs`. This happens automatically for schema files in any `\schemas\` folder. You can opt-out of this by adding `// export const optOut = true` somewhere in the file.
+
diff --git a/apps/docs/pages/@app-core/schemas/_meta.ts b/apps/docs/pages/@app-core/schemas/_meta.ts
new file mode 100644
index 0000000..7437894
--- /dev/null
+++ b/apps/docs/pages/@app-core/schemas/_meta.ts
@@ -0,0 +1,5 @@
+
+export default {
+ 'HealthCheckOutput': 'HealthCheckOutput',
+ 'HealthCheckInput': 'HealthCheckInput',
+}
diff --git a/apps/docs/pages/@app-core/screens/FormsScreen.mdx b/apps/docs/pages/@app-core/screens/FormsScreen.mdx
new file mode 100644
index 0000000..8f26809
--- /dev/null
+++ b/apps/docs/pages/@app-core/screens/FormsScreen.mdx
@@ -0,0 +1,65 @@
+import { FormsScreen, getDocumentationProps } from '@app/screens/FormsScreen'
+import { ComponentDocs } from '@app/core/mdx/ComponentDocs'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+import { FileTree, Callout } from 'nextra/components'
+
+
+ ## FormsScreen
+
+
+# FormsScreen
+
+```typescript copy
+import { FormsScreen } from '@app/screens/FormsScreen'
+```
+
+
+ ### Interactive Preview
+
+
+
+ ### Code Example
+
+
+
+ ## FormsScreen Props
+
+
+
+
+
+## Source Code
+
+
+### File Location
+
+You can find the source of the `FormsScreen` component in the following location:
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Other
+
+### Disclaimer - Automatic Docgen
+
+
+These dynamic component docs were auto-generated with `npm run regenerate-docs`. You can hook into automatic docgen by exporting `getDocumentationProps` from a component file. You'll want to provide example props from the ComponentProps zod schema, e.g:
+
+
+```tsx /getDocumentationProps/ /documentationProps/ copy filename="FormsScreen.tsx"
+/* --- Docs ---------------------- */
+
+export const getDocumentationProps = FormsScreenProps.documentationProps('FormsScreen')
+```
diff --git a/apps/docs/pages/@app-core/screens/_meta.ts b/apps/docs/pages/@app-core/screens/_meta.ts
new file mode 100644
index 0000000..beb9c5b
--- /dev/null
+++ b/apps/docs/pages/@app-core/screens/_meta.ts
@@ -0,0 +1,4 @@
+
+export default {
+ 'FormsScreen': 'FormsScreen',
+}
diff --git a/apps/docs/pages/@green-stack-core/_meta.ts b/apps/docs/pages/@green-stack-core/_meta.ts
new file mode 100644
index 0000000..f0f4b84
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/_meta.ts
@@ -0,0 +1,4 @@
+
+export default {
+ 'schemas': 'schemas',
+}
diff --git a/apps/docs/pages/@green-stack-core/components/Image.mdx b/apps/docs/pages/@green-stack-core/components/Image.mdx
new file mode 100644
index 0000000..97e964b
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/components/Image.mdx
@@ -0,0 +1,261 @@
+import { Image } from '@app/primitives'
+import { FileTree, Callout } from 'nextra/components'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+
+
+ ## Usage - `Image`
+
+
+
+
+# Universal `Image` component
+
+```tsx copy
+import { Image } from '@green-stack/components/Image'
+```
+
+### Platform Optimized Images
+
+Some primitives like the `Image` component have optimized versions for each environment:
+
+- `next/image` for web
+- `expo-image` for mobile
+
+To automatically use the right one per render context, we've provided our own universal `Image` component:
+
+```tsx copy
+
+```
+
+Which you might wish to wrap with Nativewind to provide class names to:
+
+```tsx {5} /styled/2 filename="styled.tsx"
+import { Image as UniversalImage } from '@green-stack/components/Image'
+// ☝️ Import the universal Image component
+import { styled } from 'nativewind'
+
+// ⬇⬇⬇
+
+export const Image = styled(UniversalImage, '')
+// ☝️ Adds the ability to assign tailwind classes
+```
+
+
+
+### Image Props
+
+| Prop | Type | Default | Description |
+|------|------|---------|-------------|
+| `src` | `string \| StaticImageData` | Required | Image source - can be URL or imported image |
+| `alt` | `string` | `'Alt description missing in image'` | Alt text for accessibility and SEO |
+| `width` | `number \| string` | Required* | Width in pixels or percentage |
+| `height` | `number \| string` | Required* | Height in pixels or percentage |
+| `className` | `string` | - | Tailwind classes for styling |
+| `style` | `StyleProp` | `{}` | Additional styles |
+| `priority` | `'high' \| 'normal'` | `'normal'` | Loading priority - 'high' for LCP elements |
+| `onError` | `(error: any) => void` | - | Called on image loading error |
+| `onLoadEnd` | `() => void` | - | Called when image load completes |
+| `fill` | `boolean` | - | Fill parent container (requires relative positioning) |
+| `contentFit` | `'cover' \| 'contain' \| 'fill' \| 'none' \| 'scale-down'` | `'cover'` | How image fits container |
+| `cachePolicy` | `'none' \| 'disk' \| 'memory' \| 'memory-disk'` | `'disk'` | Image caching strategy |
+| `blurRadius` | `number` | `0` | Blur effect radius in points |
+| `quality` | `number` | `75` | Image quality (1-100) |
+| `sizes` | `string` | - | Responsive image sizes hint |
+| `unoptimized` | `boolean` | `false` | Skip image optimization |
+
+*Required unless using `fill` or static import
+
+
+TypeScript Definition
+
+You can find the TypeScript definition for our Universal `Image` component in `Image.types.ts`:
+
+```typescript filename="Image.types.ts"
+type UniversalImageProps = {
+
+ // -- Universal props --
+
+ /**
+ * Universal, will affect both Expo & Next.js - Must be one of the following:
+ * - A path string like `'/assets/logo.png'`. This can be either an absolute external URL, or an internal path depending on the loader prop.
+ * - A statically imported image file, like `import logo from './logo.png'` or `require('./logo.png')`.
+ *
+ * When using an external URL, you must add it to `remotePatterns` in `next.config.js`.
+ * @platform web, android, ios @framework expo, next.js */
+ src: string | StaticImport
+
+ width?: number | `${number}` | `${number}%`
+ height?: number | `${number}` | `${number}%`
+
+ /** Universal, will affect both Expo & Next.js
+ * - Remember that the required width and height props can interact with your styling. If you use styling to modify an image's width, you should also style its height to auto to preserve its intrinsic aspect ratio, or your image will be distorted. */
+ className?: string
+
+ /** Universal, will affect both Expo & Next.js
+ * - Remember that the required width and height props can interact with your styling. If you use styling to modify an image's width, you should also style its height to auto to preserve its intrinsic aspect ratio, or your image will be distorted. */
+ style?: StyleProp | ExpoImageProps['style']
+
+ alt?: string
+ priority?: "low" | "normal" | "high" | null
+
+ onError?: ((event: ImageErrorEventData) => void)
+ onLoadEnd?: (() => void)
+
+ // -- '@next/image' specific props --
+
+ /** Custom function used to resolve image URLs. A loader is a function returning a URL string for the image, given the following parameters: `src`, `width`, `quality` (`number` from 0 - 1) Alternatively, you can use the [loaderFile](https://nextjs.org/docs/pages/api-reference/components/image#loaderfile) configuration in next.config.js to configure every instance of next/image in your application, without passing a prop. */
+ loader?: ImageLoader
+
+ fill?: boolean
+ sizes?: string
+ quality?: number | `${number}`
+ nextPlaceholder?: PlaceholderValue | 'blur' | 'empty' | `data:image/${string}`
+ loading?: 'lazy' | 'eager'
+ blurDataURL?: string
+ unoptimized?: boolean
+
+ // -- 'expo-image' specific props --
+
+ accessibilityLabel?: string
+ accessible?: boolean
+ allowDownscaling?: boolean
+ autoplay?: boolean
+ blurRadius?: number
+ cachePolicy?: 'none' | 'disk' | 'memory' | 'memory-disk'
+ contentFit?: ImageContentFit | 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'
+ contentPosition?: ImageContentPosition | 'top' | 'bottom' | 'left' | 'right' | 'center' | 'top left' | 'top right' | ...
+ enableLiveTextInteraction?: ExpoImageProps['enableLiveTextInteraction']
+ focusable?: boolean
+ expoPlaceholder?: ExpoImageProps['expoPlaceholder'] | string | StaticImport
+ onLoadStart?: (() => void)
+ onProgress?: ((event: ImageProgressEventData) => void)
+ placeholderContentFit?: ImageContentFit | 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'
+ recyclingKey?: string | null
+ responsivePolicy?: 'static' | 'initial' | 'live'
+}
+```
+
+
+
+
+Zod Schema
+
+```typescript
+import { z } from 'zod'
+
+const UniversalImageSchema = z.object({
+ // Universal props
+ src: z.union([z.string(), z.any()]), // StaticImageData type
+ alt: z.string().optional(),
+ width: z.union([z.number(), z.string()]).optional(),
+ height: z.union([z.number(), z.string()]).optional(),
+ className: z.string().optional(),
+ style: z.any().optional(),
+ priority: z.enum(['high', 'normal']).optional(),
+ onError: z.function().optional(),
+ onLoadEnd: z.function().optional(),
+
+ // Next.js specific
+ loader: z.function().optional(),
+ fill: z.boolean().optional(),
+ sizes: z.string().optional(),
+ quality: z.number().min(1).max(100).optional(),
+ nextPlaceholder: z.enum(['blur', 'empty']).optional(),
+ loading: z.enum(['lazy', 'eager']).optional(),
+ blurDataURL: z.string().optional(),
+ unoptimized: z.boolean().optional(),
+
+ // Expo specific
+ accessibilityLabel: z.string().optional(),
+ accessible: z.boolean().optional(),
+ allowDownscaling: z.boolean().optional(),
+ autoplay: z.boolean().optional(),
+ blurRadius: z.number().optional(),
+ cachePolicy: z.enum(['none', 'disk', 'memory', 'memory-disk']).optional(),
+ contentFit: z.enum(['cover', 'contain', 'fill', 'none', 'scale-down']).optional(),
+ contentPosition: z.string().optional(),
+ enableLiveTextInteraction: z.boolean().optional(),
+ focusable: z.boolean().optional(),
+ expoPlaceholder: z.any().optional(),
+ onLoadStart: z.function().optional(),
+ onProgress: z.function().optional(),
+ placeholderContentFit: z.enum(['cover', 'contain', 'fill', 'none', 'scale-down']).optional(),
+ recyclingKey: z.string().optional(),
+ responsivePolicy: z.enum(['static', 'initial', 'live']).optional(),
+})
+```
+
+
+
+
+
+## React Portability Patterns
+
+Both Next.js and Expo have their own optimized `Image` components. This is why there are also versions specifically for each of those environments:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+- `Image.next.tsx` uses `next/image` for web
+- `Image.expo.tsx` uses `expo-image` for mobile
+- `Image.types.ts` ensures there is a shared type for both implementations
+
+Finally, `Image.tsx` will retrieve whichever implementation was provided as `contextImage` to the `` component, which is further passed to ``:
+
+```tsx /.expo/ /useExpoImage/ filename="ExpoRootLayout.tsx" copy
+import { Image as ExpoImage } from '@green-stack/components/Image.expo'
+
+// ... Later ...
+
+
+ ...
+
+```
+
+```tsx /.next/ /useNextImage/ filename="NextRootLayout.tsx" copy
+import { Image as NextImage } from '@green-stack/components/Image.next'
+
+// ... Later ...
+
+
+ ...
+
+```
+
+### Why this pattern?
+
+The 'React Portability Patterns' used here are designed to ensure that you can easily reuse optimized versions of components across different flavours of writing React.
+
+On the one hand, that means it's already set up to work with both Expo and Next.js in an optimal way.
+
+But, you can actually add your own implementations for other environments, without having to refactor the code that uses the `Image` component.
+
+### Supporting more environments
+
+Just add your own `Image..tsx` file that respects the shared types, and then pass it to the `` component as `contextImage`.
diff --git a/apps/docs/pages/@green-stack-core/components/_meta.ts b/apps/docs/pages/@green-stack-core/components/_meta.ts
new file mode 100644
index 0000000..c2ade70
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/components/_meta.ts
@@ -0,0 +1,4 @@
+
+export default {
+ 'Image': 'Image',
+}
diff --git a/apps/docs/pages/@green-stack-core/generators/_meta.ts b/apps/docs/pages/@green-stack-core/generators/_meta.ts
new file mode 100644
index 0000000..58c79f9
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/generators/_meta.ts
@@ -0,0 +1,11 @@
+
+export default {
+ 'add-workspace': 'add-workspace',
+ 'add-script': 'add-script',
+ 'add-schema': 'add-schema',
+ 'add-route': 'add-route',
+ 'add-resolver': 'add-resolver',
+ 'add-generator': 'add-generator',
+ 'add-form': 'add-form',
+ 'add-dependencies': 'add-dependencies',
+}
diff --git a/apps/docs/pages/@green-stack-core/generators/add-dependencies.mdx b/apps/docs/pages/@green-stack-core/generators/add-dependencies.mdx
new file mode 100644
index 0000000..cbd0411
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/generators/add-dependencies.mdx
@@ -0,0 +1,56 @@
+import { FileTree } from 'nextra/components'
+import { Image } from '@app/primitives'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+
+
+ ## `add-dependencies` - Script
+
+
+
+
+# `add-dependencies`
+
+```md copy
+npm run add:dependencies -- --args
+```
+
+```md copy
+npx turbo gen dependencies --args
+```
+
+Less of a generator and more of a utility script, this generator **installs Expo SDK compatible versions** of the specified dependencies in the selected workspace's `package.json`.
+
+
+
+
+
+
+
+
+
+
+
+
+
+### Prompt Arguments
+
+| Argument | Type | Question / Description |
+|---------------|--------------|-----------------------------------------------------------------------------------------------------------|
+| workspacePath | autocomplete | Where would you like to install these dependencies? => e.g. `@app/core` or `some-package` |
+| dependencies | text | Which dependencies should we install Expo SDK compatible versions for? => `string` (comma separated) |
+
+
+
+### Resulting File Changes
+
+```bash
+# Installs dependencies in the selected workspace's package.json ❇️
+```
+
+
diff --git a/apps/docs/pages/@green-stack-core/generators/add-form.mdx b/apps/docs/pages/@green-stack-core/generators/add-form.mdx
new file mode 100644
index 0000000..41af0e8
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/generators/add-form.mdx
@@ -0,0 +1,57 @@
+import { FileTree } from 'nextra/components'
+import { Image } from '@app/primitives'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+
+
+ ## `add-form` - Generator
+
+
+
+
+# `add-form`
+
+```md copy
+npm run add:form -- --args
+```
+
+```md copy
+npx turbo gen add-form --args
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+### Prompt Arguments
+
+| Argument | Type | Question / Description |
+|---------------|--------------|-----------------------------------------------------------------------------------|
+| workspacePath | autocomplete | Where would you like to add this form? => e.g. `features/@app-core` |
+| formSchema | autocomplete | Which zod / input schema should we use? => e.g. `HealthCheckInput` / `new` |
+| formHookName | text | What should the form hook be named? => `string` (e.g. `useSomeSchemaState`) |
+
+
+
+### Resulting File Changes
+
+```bash
+/{workspacePath}/ # <- e.g. 'features/@app-core/' or 'packages/some-package/'
+ └── hooks/
+ └── {formHookName}.ts ❇️ # <- Will integrate with chosen / new schema
+```
+
+
diff --git a/apps/docs/pages/@green-stack-core/generators/add-generator.mdx b/apps/docs/pages/@green-stack-core/generators/add-generator.mdx
new file mode 100644
index 0000000..b30256f
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/generators/add-generator.mdx
@@ -0,0 +1,58 @@
+import { FileTree } from 'nextra/components'
+import { Image } from '@app/primitives'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+
+
+ ## `add-generator` - Generator
+
+
+
+
+# `add-generator`
+
+```md copy
+npm run add:generator -- --args
+```
+
+```md copy
+npx turbo gen generator --args
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+### Prompt Arguments
+
+| Argument | Type | Question / Description |
+|-----------------|--------------|--------------------------------------------------------------------------------|
+| workspacePath | autocomplete | Where would you like to add this generator? => e.g. `features/@app-core` |
+| generatedEntity | text | What is being generated? => `string` (e.g. `component`) |
+
+
+
+### Resulting File Changes
+
+```bash
+/{workspacePath}/ # <- e.g. 'features/@app-core/' or 'packages/some-package/'
+ └── generators/
+ └── add-{generatedEntity}.ts ❇️
+
+package.json ➕ # <- adds a root package.json script as well
+```
+
+
diff --git a/apps/docs/pages/@green-stack-core/generators/add-resolver.mdx b/apps/docs/pages/@green-stack-core/generators/add-resolver.mdx
new file mode 100644
index 0000000..f9c5e77
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/generators/add-resolver.mdx
@@ -0,0 +1,75 @@
+import { FileTree } from 'nextra/components'
+import { Image } from '@app/primitives'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+
+
+ ## `add-resolver` - Generator
+
+
+
+
+# `add-resolver`
+
+```md copy
+npm run add:resolver -- --args ...
+```
+
+```md copy
+npx turbo gen resolver --args ...
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+### Prompt Arguments
+
+| Argument | Type | Question / Description |
+|---------------------|--------------|------------------------------------------------------------------------------------------------------------------------------|
+| workspacePath | autocomplete | Where would you like to add this resolver? => e.g. `features/@app-core` |
+| resolverName | text | What is the resolver name? => `string` |
+| resolverDescription | text | Optional description: What will this data resolver do? => `string` |
+| resolverType | radio | Will this resolver query or mutate data? => `query` / `mutation` |
+| generatables | checklist | What would you like to generate linked to this resolver? => `GRAPHQL` / `GET` / `POST` / `PUT` / `DELETE` / `formHook` |
+| inputSchemaTarget | autocomplete | Which schema should we use for the resolver inputs? => e.g. `HealthCheckInput` |
+| inputSchemaName | text | What will you call this new input schema? => `string` |
+| outputSchemaTarget | autocomplete | Which schema should we use for the resolver output? => e.g. `HealthCheckOutput` |
+| outputSchemaName | text | What will you call this new output schema? => `string` |
+| apiPath | text | What API path would you like to use for REST? => e.g. `/api/some/endpoint/with/[params]/` |
+| formHookName | text | What should the form hook be called? => e.g. `useSomeResolver` |
+
+
+
+### Resulting File Changes
+
+```bash
+/apps/next/
+ └── app/(generated)/{routePath}/route.ts ❇️ # <- e.g. '/api/some/endpoint/with/[params]/' (Next.js route handler)
+
+/{workspacePath}/ # <- e.g. 'features/@app-core/' or 'packages/some-package/'
+ └── resolvers/
+ └── {resolverName}.bridge.ts ❇️ # <- Bridge Metadata file for the resolver and API
+ └── {resolverName}.resolver.ts ❇️ # <- Reusable Fn with Business logic (server only)
+ └── {resolverName}.{resolverType}.ts ❇️ # <- e.g. `query` / `mutation` fetcher
+ └── routes/
+ └── {apiPath}/ ❇️ # <- e.g. '/api/some/endpoint/with/[params]/'
+ └── route.ts ❇️ # <- If `GET` / `POST` / `PUT` / `DELETE` / `GRAPHQL` was selected
+ └── hooks/
+ └── {formHookFileName}.ts ❇️ # <- If `formHook` selected, e.g. `useResolverFormState.ts`
+```
+
+
diff --git a/apps/docs/pages/@green-stack-core/generators/add-route.mdx b/apps/docs/pages/@green-stack-core/generators/add-route.mdx
new file mode 100644
index 0000000..029426e
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/generators/add-route.mdx
@@ -0,0 +1,66 @@
+import { FileTree } from 'nextra/components'
+import { Image } from '@app/primitives'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+
+
+ ## `add-route` - Generator
+
+
+
+
+# `add-route`
+
+```md copy
+npm run add:route -- --args
+```
+
+```md copy
+npx turbo gen route --args
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+### Prompt Arguments
+
+| Argument | Type | Question / Description |
+|------------------|--------------|--------------------------------------------------------------------------------|
+| workspacePath | autocomplete | Where would you like to add this route? => e.g. `features/@app-core` |
+| screenName | text | What should the screen component be called? => `string` |
+| routePath | text | What url do you want this route on? => e.g. `/some/path` |
+| initialDataTarget| autocomplete | Would you like to fetch initial data from a resolver? => `string` |
+
+
+
+### Resulting File Changes
+
+```bash
+/apps/expo/
+ └── app/(generated)/{routePath}/index.tsx ❇️ # <- e.g. '/posts/[slug]/' (expo-router)
+/apps/next/
+ └── app/(generated)/{routePath}/page.tsx ❇️ # <- e.g. '/posts/[slug]/' (Next.js app router)
+
+/{workspacePath}/ # <- e.g. 'features/@app-core/' or 'packages/some-package/'
+ └── screens/
+ └── {ScreenName}.tsx ❇️
+ └── routes/
+ └── {routePath}/ ❇️ # <- e.g. '/posts/[slug]/'
+ └── index.tsx ❇️ # -> re-exported to `@app/next` and `@app/expo`
+```
+
+
diff --git a/apps/docs/pages/@green-stack-core/generators/add-schema.mdx b/apps/docs/pages/@green-stack-core/generators/add-schema.mdx
new file mode 100644
index 0000000..22cf1aa
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/generators/add-schema.mdx
@@ -0,0 +1,57 @@
+import { FileTree } from 'nextra/components'
+import { Image } from '@app/primitives'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+
+
+ ## `add-schema` - Generator
+
+
+
+
+# `add-schema`
+
+```md copy
+npm run add:schema -- --args
+```
+
+```md copy
+npx turbo gen schema --args
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+### Prompt Arguments
+
+| Argument | Type | Question / Description |
+|------------------|--------------|---------------------------------------------------------------------------------------|
+| workspacePath | autocomplete | Where would you like to add this schema? => e.g. `features/@app-core` |
+| schemaName | text | What is the schema name? => `string` |
+| schemaDescription| text | Optional description: What data structure does this schema describe? => `string` |
+
+
+
+### Resulting File Changes
+
+```bash
+/{workspacePath}/ # <- e.g. 'features/@app-core/' or 'packages/some-package/'
+ └── schemas/
+ └── {schemaName}.schema.ts ❇️
+```
+
+
diff --git a/apps/docs/pages/@green-stack-core/generators/add-script.mdx b/apps/docs/pages/@green-stack-core/generators/add-script.mdx
new file mode 100644
index 0000000..b8e54c7
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/generators/add-script.mdx
@@ -0,0 +1,61 @@
+import { FileTree } from 'nextra/components'
+import { Image } from '@app/primitives'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+
+
+ ## `add-script` - Generator
+
+
+
+
+# `add-script`
+
+```md copy
+npm run add:script -- --args
+```
+
+```md copy
+npx turbo gen script --args
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+### Prompt Arguments
+
+| Argument | Type | Question / Description |
+|-----------------|--------------|-------------------------------------------------------------------------------|
+| workspacePath | autocomplete | Where would you like to add this script? => e.g. `features/@app-core` |
+| scriptName | text | What is the script name? => `string` |
+| scriptFileName | text | What should we name the script file? => `string` |
+
+
+
+### Resulting File Changes
+
+```bash
+/{workspacePath}/ # <- e.g. 'features/@app-core/' or 'packages/some-package/'
+ └── scripts/
+ └── {scriptFileName}.ts ❇️
+ └── package.json ➕ # <- adds the script to the package.json scripts section
+
+package.json ➕ # <- adds a root package.json script as well
+turbo.json ➕ # <- adds a turbo.json monorepo script config entry
+```
+
+
diff --git a/apps/docs/pages/@green-stack-core/generators/add-workspace.mdx b/apps/docs/pages/@green-stack-core/generators/add-workspace.mdx
new file mode 100644
index 0000000..1769a65
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/generators/add-workspace.mdx
@@ -0,0 +1,61 @@
+import { FileTree } from 'nextra/components'
+import { Image } from '@app/primitives'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+
+
+ ## `add-workspace` - Generator
+
+
+
+
+# `add-workspace`
+
+```md copy
+npm run add:workspace -- --args
+```
+
+```md copy
+npx turbo gen add-workspace --args
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+### Prompt Arguments
+
+| Argument | Type | Question / Description |
+|--------------------|-----------|----------------------------------------------------------------------------------------------------------------------------------|
+| workspaceType | radio | What type of workspace would you like to generate? => `package` / `feature` |
+| folderName | text | What foldername do you want to give this workspace? => `string` |
+| packageName | text | What package name would you like to import from? => `string` |
+| workspaceStructure | checklist | Optional: What will this workspace contain? => `schemas`, `resolvers`, `components`, `hooks`, `screens`, `routes`, ... |
+| packageDescription | text | Optional: How would you shortly describe the package? => `string` |
+
+
+
+### Resulting Files
+
+```bash
+/{workspaceType}/ # <- e.g. packages/ or features/
+ └── {folderName}/ ❇️
+ └── {workspaceStructure}/ ❇️ # <- e.g. schemas/, resolvers/, components/, ...
+ └── package.json ❇️
+ └── tsconfig.json ❇️
+```
+
+
diff --git a/apps/docs/pages/@green-stack-core/navigation/Link.mdx b/apps/docs/pages/@green-stack-core/navigation/Link.mdx
new file mode 100644
index 0000000..3fdcbed
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/navigation/Link.mdx
@@ -0,0 +1,202 @@
+import { Image } from '@app/primitives'
+import { FileTree, Callout } from 'nextra/components'
+import { TitleWrapper, LLMOptimized } from '@app/docs/components/Hidden'
+
+
+ ## Usage - `Link`
+
+
+
+
+# Universal `Link` component
+
+If you import the `Link` component from `@green-stack/navigation`, it will automatically use the correct navigation system for the platform you are on.
+
+```tsx copy
+import { Link } from '@green-stack/navigation'
+```
+
+However, you can also ***import it from `@app/primitives` to apply tailwind styles***:
+
+```tsx copy
+import { Link } from '@app/primitives'
+```
+
+You can use the `href` prop to navigate to a new page:
+
+```tsx {3}
+
+ See example
+
+```
+
+
+
+### `UniversalLinkProps`
+
+| Property | Type | Description |
+|------------------------|--------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| children | React.ReactNode | The content to be rendered inside the link. |
+| href | string \| HREF | The path to route to on web or mobile. String only. Hints for internal routes provided through codegen. |
+| style | | Style prop: https://reactnative.dev/docs/text#style |
+| className | string | Nativewind classNames should be applied to either the parent or children of Link. Ideally, create or use a TextLink component instead. |
+| replace | boolean | Should replace the current route without adding to the history - Default: false. |
+| onPress | | Extra handler that fires when the link is pressed. |
+| target | | Specifies where to display the linked URL. |
+| asChild | boolean | **Mobile only** - Forward props to child component. Useful for custom buttons - Default: false. |
+| push | boolean | **Mobile only** - Should push the current route, always adding to the history - Default: true. |
+| testID | string \| undefined | **Mobile only** - Used to locate this view in end-to-end tests. |
+| nativeID | string \| undefined | **Mobile only** - Used to reference react managed views from native code. @deprecated use `id` instead. |
+| id | string \| undefined | **Mobile only** - Used to reference react managed views from native code. |
+| allowFontScaling | | **Mobile only** - Specifies whether fonts should scale to respect Text Size accessibility settings. |
+| numberOfLines | | **Mobile only** - Specifies the maximum number of lines to use for rendering text. |
+| maxFontSizeMultiplier | | **Mobile only** - Specifies the maximum scale factor for text. |
+| suppressHighlighting | | **Mobile only** - When true, no visual change is made when text is pressed down. |
+| scroll | boolean | **Web only** - Whether to override the default scroll behavior - Default: false. |
+| shallow | boolean | **Web only** - Update the path of the current page without rerunning getStaticProps, getServerSideProps or getInitialProps - Default: false. |
+| passHref | boolean | **Web only** - Forces `Link` to send the `href` property to its child - Default: false. |
+| prefetch | boolean | **Web only** - Prefetch the page in the background. Any `` that is in the viewport (initially or through scroll) will be preloaded. Prefetch can be disabled by passing `prefetch={false}`. When `prefetch` is set to `false`, prefetching will still occur on hover. Pages using [Static Generation](https://docs.expo.dev/router/reference/static-rendering/) will preload `JSON` files with the data for faster page transitions. Prefetching is only enabled in production. - Default: true |
+| locale | string \| false | **Web only** - The active locale is automatically prepended. `locale` allows for providing a different locale. When `false` `href` has to include the locale as the default behavior is disabled. |
+| as | Url \| undefined | **Web only** - Optional decorator for the path that will be shown in the browser URL bar. |
+
+
+
+Here's what that would look like in TypeScript:
+
+```tsx
+type UniversalLinkProps = {
+
+ children: React.ReactNode;
+
+ /** Universal - The path to route to on web or mobile. String only. */
+ href: HREF;
+
+ /** Universal - Style prop: https://reactnative.dev/docs/text#style */
+ style?: StyleProp;
+
+ /** -!- Nativewind classNames should be applied to either the parent or children of Link. Ideally, create or use a TextLink component instead */
+ className?: string; // never;
+
+ /** Universal - Should replace the current route without adding to the history - Default: false. */
+ replace?: boolean;
+
+ /** Universal - Extra handler that fires when the link is pressed. */
+ onPress?: ((e: MouseEvent | GestureResponderEvent) => void) | null | undefined;
+
+ /** Universal - */
+ target?: "_self" | "_blank" | "_parent" | "_top" | undefined;
+
+ // - Expo -
+
+ /** Mobile only - Forward props to child component. Useful for custom buttons - Default: false */
+ asChild?: boolean;
+
+ /** Mobile only - Should push the current route, always adding to the history - Default: true */
+ push?: boolean;
+
+ /** Mobile only - Used to locate this view in end-to-end tests. */
+ testID?: string | undefined;
+
+ /** Mobile only - Used to reference react managed views from native code. @deprecated use `id` instead. */
+ nativeID?: string | undefined;
+ id?: string | undefined;
+
+ allowFontScaling?: boolean | undefined;
+ numberOfLines?: number | undefined;
+ maxFontSizeMultiplier?: number | null | undefined;
+ suppressHighlighting?: boolean | undefined;
+
+ // - Next -
+
+ /** Web only - Whether to override the default scroll behavior - Default: false */
+ scroll?: boolean;
+
+ /** Web only - Update the path of the current page without rerunning getStaticProps, getServerSideProps or getInitialProps - Default: false */
+ shallow?: boolean;
+
+ /** Web only - Forces `Link` to send the `href` property to its child - Default: false */
+ passHref?: boolean;
+
+ /** Web only - Prefetch the page in the background. Any `` that is in the viewport (initially or through scroll) will be preloaded. Prefetch can be disabled by passing `prefetch={false}`. When `prefetch` is set to `false`, prefetching will still occur on hover. Pages using [Static Generation](/docs/basic-features/data-fetching/get-static-props.md) will preload `JSON` files with the data for faster page transitions. Prefetching is only enabled in production. - Defaultvalue: true */
+ prefetch?: boolean;
+
+ /** Web only - The active locale is automatically prepended. `locale` allows for providing a different locale. When `false` `href` has to include the locale as the default behavior is disabled. */
+ locale?: string | false;
+
+ /** Web only - Optional decorator for the path that will be shown in the browser URL bar. Before Next.js 9.5.3 this was used for dynamic routes, check our [previous docs](https://github.com/vercel/next.js/blob/v9.5.2/docs/api-reference/next/link.md#dynamic-routes) to see how it worked. Note: when this path differs from the one provided in `href` the previous `href`/`as` behavior is used as shown in the [previous docs](https://github.com/vercel/next.js/blob/v9.5.2/docs/api-reference/next/link.md#dynamic-routes). */
+ as?: Url | undefined;
+
+}
+```
+
+
+
+
+
+## React Portability Patterns
+
+Each environment has it's own optimized Link component. This is why there are also versions specifically for each of those environments:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+- `Link.next.tsx` is optimized for the Next.js app router.
+- `Link.expo.tsx` is optimized for Expo Router.
+- `Link.types.ts` ensures both implementations are compatible with the same interface, allowing you to use the same `Link` component across both Expo and Next.js environments.
+
+The main `Link.tsx` retrieves whichever implementation was provided as `contextLink` to the `` component, which is further passed to ``:
+
+```tsx /.expo/ /useExpoLink/ filename="ExpoRootLayout.tsx" copy
+import { Link as ExpoLink } from '@green-stack/navigation/Link.expo'
+
+// ... Later ...
+
+
+ ...
+
+```
+
+```tsx /.next/ /useNextLink/ filename="NextRootLayout.tsx" copy
+import { Link as NextLink } from '@green-stack/navigation/Link.next'
+
+// ... Later ...
+
+
+ ...
+
+```
+
+### Why this pattern?
+
+The 'React Portability Patterns' used here are designed to ensure that you can easily reuse optimized versions of components across different flavours of writing React.
+
+On the one hand, that means it's already set up to work with both Expo and Next.js in an optimal way.
+
+But, you can actually add your own implementations for other environments, without having to refactor the code that uses the `Link` component.
+
+### Supporting more environments
+
+Just add your own `Link..tsx` file that respects the shared types, and then pass it to the `` component as `contextLink`.
diff --git a/apps/docs/pages/@green-stack-core/navigation/_meta.ts b/apps/docs/pages/@green-stack-core/navigation/_meta.ts
new file mode 100644
index 0000000..1733ccb
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/navigation/_meta.ts
@@ -0,0 +1,6 @@
+
+export default {
+ 'useRouter': 'useRouter',
+ 'useRouteParams': 'useRouteParams',
+ 'Link': 'Link',
+}
diff --git a/apps/docs/pages/@green-stack-core/navigation/useRouteParams.mdx b/apps/docs/pages/@green-stack-core/navigation/useRouteParams.mdx
new file mode 100644
index 0000000..533b47a
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/navigation/useRouteParams.mdx
@@ -0,0 +1,88 @@
+import { Image } from '@app/primitives'
+import { FileTree, Callout } from 'nextra/components'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+
+
+ ## Usage - `useRouteParams()`
+
+
+
+
+# useRouteParams()
+
+Tiny absctraction layer that retrieves the parameters of a route in both the Expo Router and Next.js app routers. Serverside, in the browser and on iOS or Android:
+
+```tsx copy
+import { useRouteParams } from '@green-stack/navigation'
+```
+
+```tsx copy
+const routeParams = useRouteParams(routeProps)
+```
+
+Typescript should complain if you don't, but make sure to include the route's screen props when using this hook, as it relies on them to access the route parameters in Next.js
+
+## React Portability Patterns
+
+Each environment has it's own ways of accessing route parameters. This is why there are also versions specifically for each of those environments:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Where `useRouteParams.next.tsx` covers the Next.js app router, and `useRouteParams.expo.tsx` covers Expo Router. The main `useRouteParams.tsx` retrieves whichever implementation was provided to the `` component and, which is further passed to ``:
+
+```tsx /.expo/ /useExpoRouteParams/ filename="ExpoRootLayout.tsx" copy
+import { useRouteParams as useExpoRouteParams } from '@green-stack/navigation/useRouteParams.expo'
+
+// ... Later ...
+
+
+ ...
+
+```
+
+```tsx /.next/ /useNextRouteParams/ filename="NextRootLayout.tsx" copy
+import { useRouteParams as useNextRouteParams } from '@green-stack/navigation/useRouteParams.next'
+
+// ... Later ...
+
+
+ ...
+
+```
+
+While the `useRouteParams.types.ts` file ensures both implementations are compatible with the same interface, allowing you to use the same `useRouteParams()` hook across both Expo and Next.js environments.
+
+### Why this pattern?
+
+The 'React Portability Patterns' used here are designed to ensure that you can easily reuse optimized versions of hooks across different flavours of writing React.
+
+On the one hand, that means it's already set up to work with both Expo and Next.js in an optimal way.
+
+But, you can actually add your own implementations for other environments, without having to refactor the code that uses the `useRouteParams` hook.
+
+### Supporting more environments
+
+Just add your own `useRouteParams..ts` file that respects the shared types, and then pass it to the `` component as `useContextRouteParams`.
+
+
diff --git a/apps/docs/pages/@green-stack-core/navigation/useRouter.mdx b/apps/docs/pages/@green-stack-core/navigation/useRouter.mdx
new file mode 100644
index 0000000..2c69e16
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/navigation/useRouter.mdx
@@ -0,0 +1,104 @@
+import { Image } from '@app/primitives'
+import { FileTree, Callout } from 'nextra/components'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+
+
+ ## Usage - `useRouter()`
+
+
+
+
+# Universal `useRouter()` hook
+
+```tsx
+import { useRouter } from '@green-stack/navigation/useRouter'
+```
+
+You can use `router.push()` to navigate to a new page:
+
+```tsx
+const router = useRouter()
+
+router.push('/examples/[slug]', '/examples/123')
+```
+
+> `.push()` will use a push operation on mobile if possible.
+
+There are also other methods available on the `router` object:
+
+- `router.navigate()` - Navigate to the provided href
+- `router.replace()` - Navigate without appending to the history
+- `router.back()` - Go back in the history
+- `router.canGoBack()` - Check if there's history that supports invoking the `back()` function
+- `router.setParams()` - Update the current route query params without navigating
+
+
+
+## React Portability Patterns
+
+Each environment has it's own optimized router. This is why there are also versions specifically for each of those environments:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Where `useRouter.next.ts` covers the Next.js app router, and `useRouter.expo.ts` covers Expo Router. The main `useRouter.ts` retrieves whichever implementation was provided to the `` component, which is further passed to ``:
+
+```tsx /.expo/ /useExpoRouter/ filename="ExpoRootLayout.tsx" copy
+import { useRouter as useExpoRouter } from '@green-stack/navigation/useRouter.expo'
+
+// ... Later ...
+
+const expoContextRouter = useExpoRouter()
+
+
+ ...
+
+```
+
+```tsx /.next/ /useNextRouter/ filename="NextRootLayout.tsx" copy
+import { useRouter as useNextRouter } from '@green-stack/navigation/useRouter.next'
+
+// ... Later ...
+
+const nextContextRouter = useNextRouter()
+
+
+ ...
+
+```
+
+While the `useRouter.types.ts` file ensures both implementations are compatible with the same interface, allowing you to use the same `useRouter()` hook across both Expo and Next.js environments.
+
+### Why this pattern?
+
+The 'React Portability Patterns' used here are designed to ensure that you can easily reuse optimized versions of hooks across different flavours of writing React.
+
+On the one hand, that means it's already set up to work with both Expo and Next.js in an optimal way.
+
+But, you can actually add your own implementations for other environments, without having to refactor the code that uses the `useRouter` hook.
+
+### Supporting more environments
+
+Just add your own `useRouter..ts` file that respects the shared types, and then pass it to the `` component as `contextRouter`.
+
+
diff --git a/apps/docs/pages/@green-stack-core/schemas.mdx b/apps/docs/pages/@green-stack-core/schemas.mdx
new file mode 100644
index 0000000..308cfc6
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/schemas.mdx
@@ -0,0 +1,629 @@
+import { Image } from '@app/primitives'
+import { FileTree, Callout } from 'nextra/components'
+import { Hidden, TitleWrapper, LLMOptimized } from '@app/docs/components/Hidden'
+
+
+ ## Intro - `schema()`
+
+
+
+
+# Schemas API reference
+
+```typescript copy
+import { z, schema } from '@green-stack/schemas'
+```
+
+
+
+
+
+
+
+
+
+
+
+## Building Schemas
+
+Because `schema` is a tiny wrapper around Zod (V3), it's usage is similar to using `z.object()`
+
+```typescript {4, 8} /Requires a name/1 /'User'/ /to port to other formats later/ filename="User.ts"
+export const User = schema('User', {
+ // Requires a name value (☝️) to port to other formats later, best to keep the same
+
+ // Zod can help you go even narrower than typescript
+ name: z.string().min(2), // <- e.g. Needs to be a string with at least 2 letters
+ age: z.number().min(18), // <- e.g. Age must be a number of at least 18
+
+ // Just like TS, it can help you indicate fields as optional
+ isAdmin: z.boolean().default(false), // <- Marked optional, defaults to false
+ birthdate: z.Date().nullish(), // = same as calling .nullable().optional()
+})
+```
+
+> The main difference is that you have to specify a name for each schema as the first argument. This helps us port your schemas to other formats later:
+
+- [Auto-generated MDX UI component docs](/single-sources-of-truth#automatic-mdx-docgen)
+- [Resolver Inputs, Outputs, Docs, and GraphQL definitions](/data-resolvers)
+- [Data Fetching Functions and Hooks](/data-fetching)
+- [Custom Implementations using the introspection metadata API](/single-sources-of-truth#transforming-to-other-formats)
+
+### Defining Primitives
+
+```typescript
+const someString = z.string() // -> string
+const someNumber = z.number() // -> number
+const someBoolean = z.boolean() // -> boolean
+const someDate = z.date() // -> Date
+```
+
+### Defaults and optionality
+
+You can mark fields as optional or provide default values by using either:
+- `.optional()` - to allow `undefined`
+- `.nullable()` - to allow `null`
+- `.nullish()` - to allow both
+
+You can also use `.default()` to provide a default value when the field isn't passed in:
+
+```ts {8} /.optional()/ /.nullable()/ /.nullish()/ /.default/2 filename="User.ts"
+// Define a schema with optional and nullable fields
+const User = schema('...', {
+
+ name: z.string().optional(), // <- Allow undefined
+ age: z.number().nullable(), // <- Allow null
+ birthData: z.date().nullish(), // <- Allow both
+
+ // Use .default() to make optional in args,
+ // but provide a default value when it IS undefined
+ isAdmin: z.boolean().default(false), // <- false
+})
+```
+
+> When using `.default()`, you might need to be more specifc when inferring types. You can use `z.input()` or `z.output()` to get the correct type. Based on which you choose, defaulted fields will be either optional or required.
+
+### Enums with `inputOptions()`
+
+```typescript copy
+import { inputOptions } from '@green-stack/schemas'
+```
+
+To define more flexible enums you can easily reuse in forms, we also export an `inputOptions()` function:
+
+```typescript
+const MY_ENUM = inputOptions({
+ // Define the enum values, alongside their display names
+ value1: 'Value 1',
+ value2: 'Value 2',
+ value3: 'Value 3',
+})
+
+const schemaWithEnum = schema('MySchema', {
+ myEnumField: MY_ENUM, // <- Use the enum in a schema
+})
+```
+
+This helps you create an enum definition extending Zod's `z.enum()` with some additions:
+
+You can use `.entries` object to get the original object with key-value pairs
+
+```typescript /.entries/
+MY_ENUM.entries
+// {
+// value1: 'Value 1',
+// value2: 'Value 2',
+// value3: 'Value 3',
+// }
+```
+
+You can get auto-completion for the enum values / option keys:
+
+```typescript
+MyEnum.value1 // => 'value1'
+MyEnum.enum.value1 // => 'value1' (alternatively, the way Zod Enums work)
+```
+
+You can retrieve an array of the enum values with `.options`:
+
+```typescript
+MY_ENUM.options // => ['value1', 'value2', 'value3']
+```
+
+All of this on top of what's already available in [Zod Enums](https://v3.zod.dev/?id=zod-enums).
+
+### `.extendSchema()` - add fields
+
+It can happen that you need to differentiate between two similar data shapes, for example, needing to expand on an existing shape.
+
+
+
+
+
+
+
+
+You can add new fields by calling `.extendSchema()` on the original schema:
+
+```ts {6} /.extendSchema/ filename="AdminUser.ts"
+// Extend the User schema
+const AdminUser = User.extendSchema('AdminUser', {
+ isAdmin: z.boolean().default(true),
+})
+
+type AdminUser = z.infer
+
+// {
+// name: string,
+// age: number,
+// birthDate?: Date | null,
+//
+// isAdmin?: boolean, // <- New field added
+// }
+```
+
+> You will **need to provide a new name** for the extended schema. This ensures there is no conflict with the original one when we port it to other formats.
+
+### `.pickSchema()` - select fields
+
+Similar to extending, **you can create a new schema by picking specific fields from another**:
+
+```ts {6} /.pickSchema/ filename="PublicUser.ts"
+const PublicUser = User.pickSchema('PublicUser', {
+ name: true,
+ age: true, // <- Only these fields will be included
+})
+
+type PublicUser = z.infer
+
+// {
+// name: string,
+// age: number,
+// }
+```
+
+### `.omitSchema()` - remove fields
+
+The reverse is also possible by removing certain fields from another. The new schema will have all fields from the original, *except the ones you specify*:
+
+```ts {5} /.omitSchema/ filename="PublicUser.ts"
+const PublicUser = User.omitSchema('PublicUser', {
+ birthDate: true, // <- Will be missing in the new schema
+})
+
+type PublicUser = z.infer
+
+// {
+// name: string,
+// age: number,
+// }
+```
+
+### Nesting and Collections
+
+You can nest schemas within each other.
+This is useful when you need to represent a more complex data shape
+
+
+
+
+
+
+
+
+
+
+For example, sometimes you need to represent a collection of specific data:
+
+```ts {6, 17} /z.array(User)/ /User[]/ filename="Team.ts"
+const Team = schema('Team', {
+ members: z.array(User), // <- Pass the 'User' schema to z.array()
+ teamName: z.string(),
+})
+
+type Team = z.infer
+
+// {
+// teamName: string,
+// members: {
+// name: string,
+// age: number,
+// birthDate?: Date | null,
+// }[]
+// }
+
+// ⬇⬇⬇ Which is the same as:
+
+// {
+// teamName: string,
+// members: User[]
+// }
+```
+
+## Extracting Types
+
+The main thing to use schemas for is to hard-link validation with types.
+
+You can extract the type from the schema using `z.infer()`, `z.input()` or `z.output()`:
+
+```tsx filename="User.ts"
+// Extract type from the schema and export it as a type alias
+export type User = z.infer
+
+// If you have defaults, you can use z.input() or z.output() instead
+export type UserOutput = z.output
+export type UserInput = z.input
+```
+
+`⬇⬇⬇`
+
+```tsx /?/
+// {
+// name: string,
+// age: number,
+// isAdmin?: boolean,
+// birthDate?: Date | null,
+// }
+```
+
+> In this case where we check the resulting type of `z.input()`, the 'isAdmin' field will be marked as optional, as it's supposedly not defaulted to `false` yet. If we'd inspect `z.output()`, it would be marked as required since it's either provided or presumed defaulted.
+
+### Advanced Types
+
+*Anything you can define the shape of in Typescript, you can define in Zod:*
+
+```ts {3, 6, 9}
+const Task = schema('Task', {
+
+ // Enums
+ status: z.enum(['draft', 'published', 'archived']),
+
+ // Arrays
+ tags: z.array(z.string()),
+
+ // Tuples
+ someTuple: z.tuple([z.string(), z.number()]),
+
+})
+```
+
+When extracting the type with `type Task = z.infer`:
+
+```ts
+// {
+// status: 'draft' | 'published' | 'archived',
+// tags: string[],
+// someTuple: [string, number],
+// }
+```
+
+> Check [zod.dev](https://zod.dev) for the full list of what you can define with zod.
+
+## Validating inputs
+
+You can use the `.parse()` method to validate inputs against the schema:
+
+```tsx {1, 4} /.shape.age/
+// Call .parse() on the whole User schema...
+const newUser = User.parse(someInput) // <- Auto infers 'User' type if valid
+
+// ...or validate idividual fields by using '.shape' 👇
+User.shape.age.parse("Invalid - Not a number")
+// Throws => ZodError: "Expected a number, recieved a string."
+// Luckily, TS will already catch this in your editor ( instant feedback 🙌 )
+```
+
+If a field's value does not match the schema, it will throw a `ZodError`:
+
+```ts {8, 14} /.issues/
+try {
+
+ // 🚧 Will fail validation
+ const someNumber = z.number().parse("Not a number")
+
+} catch (error) { // ⬇⬇⬇
+
+ /* Throws 'ZodError' with a .issues array:
+ [{
+ code: 'invalid_type',
+ expected: 'number',
+ received: 'string',
+ path: [],
+ message: 'Expected number, received string',
+ }]
+ */
+
+}
+```
+
+### Custom Errors
+
+You can provide custom error messages by passing the `message` prop:
+
+```ts
+const NumberValue = z.number({ message: 'Please provide a number' })
+// Throws => ZodError: [{ message: "Please provide a number", ... })
+```
+
+You can provide **custom error messages** for specific validations:
+
+```ts
+const MinimumValue = z.number().min(10, { message: 'Value must be at least 10' })
+// Throws => ZodError: [{ message: "Value must be at least 10", ... })
+
+const MaximumValue = z.number().max(100, { message: 'Value must be at most 100' })
+// Throws => ZodError: [{ message: "Value must be at most 100", ... })
+```
+
+## Security Extensions
+
+We added some additional features to Zod to help you with security and data handling.
+
+### Mark fields as `.sensitive()`
+
+```typescript
+ password: z.string().sensitive()
+```
+
+In your schemas, you can mark fields as sensitive using `.sensitive()`. This will:
+
+- Exclude the field from appearing in the GraphQL schema, introspection or queries
+- Mark the field as strippable in API resolvers / responses (*)
+- Mark the field with `isSensitive: true` in schema introspection
+
+> * 'Strippable' means when using either `withDefaults()` OR `applyDefaults()` / `formatOutput()` with the `{ stripSensitive: true }` option as second argument. If none of these are used, the field will still be present in API route handler responses, but not GraphQL.
+
+## Metadata APIs
+
+The reason we're able to use schemas as a [single source of truth](/single-sources-of-truth) to build the right abstractions around, is due to its strong metadata API:
+
+### `.introspect()`
+
+```typescript
+const metadata = User.introspect()
+```
+
+This will return a metadata object with the following type:
+
+```typescript
+type Metadata = {
+
+ // Essentials
+ name?: string, // <- The name you passed to schema(), e.g. 'User'
+ zodType: ZOD_TYPE, // e.g. 'ZodString' | 'ZodNumber' | 'ZodBoolean' | 'ZodDate' | ...
+ baseType: BASE_TYPE, // e.g. 'String' | 'Number' | 'Boolean' | 'Date' | ...
+
+ // Optionality and defaults
+ isOptional?: boolean,
+ isNullable?: boolean,
+ defaultValue?: T, // The resulting Zod type
+
+ // Documentation
+ exampleValue?: T, // The resulting Zod type
+ description?: string,
+ minLength?: number,
+ maxLength?: number,
+ exactLength?: number,
+ minValue?: number,
+ maxValue?: number,
+
+ // Flags
+ isInt?: boolean,
+ isBase64?: boolean,
+ isEmail?: boolean,
+ isURL?: boolean,
+ isUUID?: boolean,
+ isDate?: boolean,
+ isDatetime?: boolean,
+ isTime?: boolean,
+ isIP?: boolean,
+
+ // Literals, e.g. z.literal()
+ literalValue?: T, // The resulting Zod type
+ literalType?: 'string' | 'boolean' | 'number',
+ literalBase?: BASE_TYPE,
+
+ // e.g. Nested schema field(s) to represent:
+ // - object properties (like meta for 'age' / 'isAdmin' / ...)
+ // - array elements
+ // - tuple elements
+ schema?: S,
+
+ // The actual Zod object, only included with .introspect(true)
+ zodStruct?: z.ZodType & { ... }, // <- Outer zod schema (e.g. ZodDefault)
+ innerStruct?: z.ZodType & { ... }, // <- Inner zod schema (not wrapped)
+
+ // Mark as serverside only, strippable in API responses
+ isSensitive?: boolean,
+
+ // Compatibility with other systems like databases & drivers
+ isID?: boolean,
+ isIndex?: boolean,
+ isUnique?: boolean,
+ isSparse?: boolean,
+}
+```
+
+When inspecting an object, like the `User` schema we've referenced in this doc, the metadata for all the fields defined on the schema will be defined as `Record` on the `schema` property.
+
+If you need to access the zod struct that was used to create the schema or field definition, you can pass `true` as the first argument to `.introspect()`:
+
+```typescript
+const metadataWithZodStructs = User.introspect(true)
+```
+
+### `.documentationProps()`
+
+You can use schemas to generate interactive UI docs for your components by describing their props with it.
+
+You can define specific example props for the component's docs by chaining `.example()` on prop definitions, alternatively, `.default()` will be used.
+
+All that's needed afterwards, is to call `.documentationProps()` on the schema from within the component file:
+
+```typescript {2, 12} /.example/ /.documentationProps/ filename="Button.tsx"
+export const ComponentProps = schema('ComponentProps', {
+ // Define the props shape with zod, e.g.:
+ someProp; z.string().example('Some example value'),
+})
+
+/* --- --------------- */
+
+export const ComponentName = (rawProps: ComponentProps) => <>...>
+
+/* --- Documentation ------------------ */
+
+export const documentationProps = ComponentProps.documentationProps('ComponentName')
+```
+
+Doing this will indicate to the `npm run regenerate:docs` command that this component should have docs generated for it.
+
+For an example of what generated docs look like, check out the [Button component docs](/@app-core/components/Button).
+
+To further customize the docs, you can pass options that follow this type as the second argument:
+
+```typescript
+type DocumentationPropsOptions = {
+ /** -i- Pass to display specific props as starting examples for preview + props table */
+ previewProps: Record,
+ exampleProps?: Partial,
+ /** -i- If a form component, you can use this prefill the current value from the url */
+ valueProp?: keyof T | HintedKeys,
+ /** -i- If a form component, you can use this save the current value to the url */
+ onChangeProp?: keyof T | HintedKeys,
+}
+```
+
+For example, for a `` component, you can use:
+
+```typescript
+export const getDocumentationProps = SwitchProps.documentationProps('Switch', {
+ exampleProps: { checked: true }, // <- Start in the 'checked' state
+ valueProp: 'checked',
+ onChangeProp: 'onCheckedChange',
+})
+```
+
+> For an example of what this looks like, try visiting:
+> [/@app-core/forms/Switch?checked=true&label=Hello+from+the+URL](/@app-core/forms/Switch?checked=true&label=Hello+from+the+URL).
+
+## Automations
+
+### Schema generator
+
+Like all elements of our recommended way of working, there is a turborepo generator to help create a schema in a specific workspace:
+
+```shell copy
+npm run add:schema
+```
+
+`⬇⬇⬇`
+
+will prompt you for a target workspace and name:
+
+```shell
+>>> Modify "your-project" using custom generators
+
+? Where would you like to add this schema?
+❯ @app/core # -- from features/app-core
+ some-feature # -- from features/some-feature
+ some-package # -- from packages/some-package
+```
+
+`⬇⬇⬇`
+
+```shell
+>>> Modify "your-project" using custom generators
+
+? Where would you like to add this schema? # @app/core
+? What is the schema name? # SomeData
+? Optional description? # ...
+
+>>> Changes made:
+ • /features/@app-core/schemas/SomeData.ts # (add)
+ • Opened 1 file in VSCode # (open-in-vscode)
+
+>>> Success!
+```
+
+`⬇⬇⬇`
+
+```shell
+@app-core
+
+ └── /schemas/...
+ └── SomeData.schema.ts
+```
+
+
+
+Though if you chose to also generate an integration, it might look like this instead:
+
+```shell
+@app-core
+
+ └── /schemas/...
+ └── SomeData.schema.ts
+
+ └── /hooks/...
+ └── useSomeData.ts # <- Form state hook using `useFormState()`
+
+ └── /models/...
+ └── SomeData.ts # <- `@db/driver` model using `createSchemaModel()`
+```
+
+
+
+## Disclaimers
+
+### About `z.union` and `z.tuple`
+
+Since GraphQL and other systems might not natively support unions or tuples, it could be best to avoid them. We allow you to define them, but tranformations of these to other formats is considered experimental and potentially not entirely type-safe.
+
+You can always use `z.array` and `z.object` to represent the same data shapes.
+
+e.g. instead of:
+
+```ts
+const someUnionField = z.union([z.string(), z.number()])
+// TS: string | number
+
+const someTupleField = z.tuple([z.string(), z.number()])
+// TS: [string, number]
+```
+
+you might try this instead:
+
+```ts
+const SomeSchema = schema('SomeSchema', {
+
+ // Unions
+ someUnionFieldStr: z.string(), // TS: string
+ someUnionFieldNum: z.number(), // TS: number
+
+ // Tuples
+ someTupleField: schema('SomeTupleField', {
+ stringValue: z.string(), // TS: string
+ numberValue: z.number(), // TS: number
+ })
+})
+```
+
+... which will work in GraphQL and other formats as well.
+
+We've done our best to hack in experimental tuple and union support where possible. You can take it as-is, edit it further to your liking or avoid tuples and unions entirely. *The choice is yours.*
+
+## Further reading
+
+From our own docs:
+- [Single Sources of Truth](/single-sources-of-truth) - Core Starterkit Concept
+- [Data Bridges for fetching](/data-resolvers#start-from-a-databridge) - Starterkit Docs
+
+Relevant external resources:
+- [Zod's official docs](https://zod.dev) - zod.dev
+- [The Joy of Single Sources of Truth](https://dev.to/codinsonn/the-joy-of-single-sources-of-truth-277o) - Blogpost by [Thorr ⚡️ @codinsonn.dev](https://codinsonn.dev)
diff --git a/apps/docs/pages/@green-stack-core/schemas/_meta.ts b/apps/docs/pages/@green-stack-core/schemas/_meta.ts
new file mode 100644
index 0000000..3bed75a
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/schemas/_meta.ts
@@ -0,0 +1,8 @@
+
+export default {
+ 'createResolver': 'createResolver',
+ 'createNextRouteHandler': 'createNextRouteHandler',
+ 'createGraphResolver': 'createGraphResolver',
+ 'createDataBridge': 'createDataBridge',
+ 'bridgedFetcher': 'bridgedFetcher',
+}
diff --git a/apps/docs/pages/@green-stack-core/schemas/bridgedFetcher.mdx b/apps/docs/pages/@green-stack-core/schemas/bridgedFetcher.mdx
new file mode 100644
index 0000000..4d46715
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/schemas/bridgedFetcher.mdx
@@ -0,0 +1,85 @@
+import { Image } from '@app/primitives'
+import { FileTree } from 'nextra/components'
+import { TitleWrapper, Hidden } from '@app/docs/components/Hidden'
+
+
+ ## `bridgedFetcher()`
+
+
+
+
+# `bridgedFetcher()`
+
+```typescript copy
+import { bridgedFetcher } from '@green-stack/schemas/bridgedFetcher'
+```
+
+
+
+
+
+
+
+
+
+
+
+A util to turn a [Data Bridge](/@green-stack-core/schemas/createDataBridge) into a universal fetcher function that can be used in your front-end code with, for example, `react-query`.
+
+
+ ### Creating a bridged fetcher
+
+
+```ts {3} /bridgedFetcher/3 filename="updatePost.query.ts"
+import { updatePostBridge } from './updatePost.bridge'
+// ☝️ Reuse your data bridge
+import { bridgedFetcher } from '@green-stack/schemas/bridgedFetcher'
+// ☝️ Universal graphql fetcher that can be used in any JS environment
+
+/* --- updatePostFetcher() --------- */
+
+export const updatePostFetcher = bridgedFetcher(updatePostBridge)
+```
+
+
+
+### Fetching with `react-query`
+
+```typescript copy
+import { useQuery } from '@tanstack/react-query'
+```
+
+For simply fetching data with `react-query`, you can use the `useQuery` hook with the `bridgedFetcher`:
+
+```typescript copy
+useQuery({
+ queryKey: ['updatePost', postId],
+ queryFn: someBridgedFetcher,
+ // provide the fetcher (👆) as the queryFn
+ ...options,
+})
+```
+
+Be sure to check the [`useQuery()`](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery) docs for all the available options you might want to prefill or abstract.
+
+You can also do [mutations](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation) with `useMutation()` for updating data in any way.
+
+
+
+## Related Automations
+
+### [`add-resolver`](/@green-stack-core/generators/add-resolver) - generator
+
+You don't need to manually create a new fetcher with `bridgedFetcher()` every time you create a new resolver. You can use the [`add-resolver`](/@green-stack-core/generators/add-resolver) generator to create a new resolver and its fetching function in one go:
+
+```bash copy
+npx turbo gen resolver
+```
+
+
diff --git a/apps/docs/pages/@green-stack-core/schemas/createDataBridge.mdx b/apps/docs/pages/@green-stack-core/schemas/createDataBridge.mdx
new file mode 100644
index 0000000..65676eb
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/schemas/createDataBridge.mdx
@@ -0,0 +1,347 @@
+import { Image } from '@app/primitives'
+import { FileTree } from 'nextra/components'
+import { TitleWrapper, Hidden } from '@app/docs/components/Hidden'
+
+
+ ## `createDataBridge()`
+
+
+
+
+# `createDataBridge()`
+
+```typescript copy
+import { createDataBridge } from '@green-stack/schemas/createDataBridge'
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+To create a `DataBridge`, simply provide call `createDataBridge()` with with the input and output schemas and some additional metadata:
+
+```typescript copy
+const healthCheckBridge = createDataBridge({
+
+ // Input and Output
+ inputSchema: HealthCheckInput,
+ outputSchema: HealthCheckOutput,
+
+ // Basic Metadata
+ resolverName: 'healthCheck',
+ resolverType: 'query', // 'query' | 'mutation'
+
+ // REST Metadata
+ apiPath: '/api/health',
+ allowedMethods: ['GET'], // 'GET' | 'POST' | 'PUT' | 'DELETE' | 'GRAPHQL'
+
+ // Optional Metadata
+ resolverArgsName?: 'healthCheckArgs', // Custom Args name for GraphQL schema and queries
+
+})
+```
+
+
+
+## Why "Data Bridges"?
+
+[Schemas](/@green-stack-core/schemas) serve as the [single source of truth](/single-sources-of-truth) for your data shape. But what about the shape of your APIs?
+
+By combining input and output schemas into a `bridge` file, and adding some API metadata, bridges serve as the source of truth for your API resolver:
+
+### Reusable Client-Server Contract
+
+Think of a **"Databridge"** as a literal *bridge between the front and back-end*.
+
+It's a metadata object you can use from either side to provide / transform / extract:
+
+- ✅ **Route handler args** from request params / body
+- ✅ Input and output **types + validation + defaults**
+- ✅ GraphQL **schema definitions** for `schema.graphql`
+- ✅ The query string to call our GraphQL API with
+
+### Recommended file conventions
+
+
+
+
+
+
+
+
+
+
+
+
+*There's two reasons we suggest you define this "DataBridge" in a separate file*:
+
+- 1. **Reusability**: If kept separate from business logic, you can reuse it in both front and back-end code.
+- 2. **Consistency**: Predicatable patterns make it easier to build automations and generators around them.
+
+> For this reason, we suggest you add `.bridge.ts` to your bridge filenames.
+
+
+
+## Using a `DataBridge`
+
+You can use the resulting `DataBridge` in various ways, depending on your needs:
+
+### Flexible Resolvers
+
+To use the data bridge we just created to bundle together the input and output types with our business logic, create a new resolver file and passing the bridge as the final arg to [`createResolver()`](/@green-stack-core/schemas/createResolver) at the end.
+
+The first argument is your resolver function will contain a function with your business logic:
+
+```ts {7, 17, 22} /updatePostBridge/ /createResolver/1,3 filename="updatePost.resolver.ts"
+import { createResolver } from '@green-stack/schemas/createResolver'
+import { updatePostBridge } from './updatePost.bridge'
+import { Posts } from '@db/models'
+
+/** --- updatePost() ---- */
+/** -i- Update a specific post. */
+export const updatePost = createResolver(async ({
+ args, // <- Auto inferred types (from 'inputSchema')
+ context, // <- Request context (from middleware)
+ parseArgs, // <- Input validator (from 'inputSchema')
+ withDefaults, // <- Response helper (from 'outputSchema')
+}) => {
+
+ // Validate input and apply defaults, infers input types as well
+ const { slug, ...postUpdates } = parseArgs(args)
+
+ // -- Context / Auth Guards / Security --
+
+ // e.g. use the request 'context' to log out current user
+ const { user } = context // example, requires auth middleware
+
+ // -- Business Logic --
+
+ // e.g. update the post in the database
+ const updatedPost = await Posts.updateOne({ slug }, postUpdates)
+
+ // -- Respond --
+
+ // Typecheck response and apply defaults from bridge's outputSchema
+ return withDefaults({
+ slug,
+ title,
+ content,
+ })
+
+}, updatePostBridge)
+```
+
+> You pass the bridge (☝️) as the second argument to `createResolver()` to:
+- 1️⃣ infer the input / arg types from the bridge's `inputSchema`
+- 2️⃣ enable `parseArgs()` and `withDefaults()` helpers for validation, hints + defaults
+
+> **The resulting function can be used as just another async function** anywhere in your back-end.
+
+> The difference with a regular function, since the logic is bundled together with its data-bridge / input + output metadata, is that we can easily transform it into APIs:
+
+
+
+### API Route Handlers
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+You can create a new API route by exporting a `GET` / `POST` / `UPDATE` / `DELETE` handler assigned to a `createNextRouteHandler()` that wraps your "bridged resolver":
+
+```ts /createNextRouteHandler/1,3 /updatePost/4 filename="@some-feature / routes / api / posts / [slug] / update / route.ts"
+import { updatePost } from '@app/resolvers/updatePost.resolver'
+import { createNextRouteHandler } from '@green-stack/schemas/createNextRouteHandler'
+
+/* --- Routes ------------ */
+
+export const UPDATE = createNextRouteHandler(updatePost)
+// Automatically extracts (☝️) args from url / search params
+// based on the zod 'inputSchema'
+
+// If you want to support e.g. POST (👇), same deal (checks request body too)
+export const POST = createNextRouteHandler(updatePost)
+```
+
+What `createNextRouteHandler()` does under the hood:
+- 1. extract the input from the request context
+- 2. validate it
+- 3. call the resolver function with the args (and e.g. token / session / request context)
+- 4. return the output from your resolver with defaults applied
+
+> Check [Next.js Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) to understand supported exports (like `GET` or `POST`) and their options.
+
+> Restart your dev server or run `npm run link:routes` to make sure your new API route is available.
+
+
+
+### GraphQL Resolvers
+
+We made it quite easy to enable GraphQL for your resolvers. The flow is quite similar.
+
+*In the same file*, add the following:
+
+```ts {3, 11} filename="features / @app-core / routes / api / health / route.ts"
+import { updatePost } from '@app/resolvers/updatePost.resolver'
+import { createNextRouteHandler } from '@green-stack/schemas/createNextRouteHandler'
+import { createGraphResolver } from '@green-stack/schemas/createGraphResolver'
+
+/* --- Routes ------------ */
+
+// exports of `GET` / `POST` / `PUT` / ...
+
+/* --- GraphQL ----------- */
+
+export const graphResolver = createGraphResolver(updatePost)
+// Automatically extracts input (☝️) from graphql request context
+```
+
+After exporting `graphResolver` here, restart the dev server or run `npm run build:schema` manually.
+
+This will:
+- 1. pick up the `graphResolver` export
+- 2. put it in our list of graphql compatible resolvers at `resolvers.generated.ts` in `@app/registries`
+- 3. recreate `schema.graphql` from input & output schemas from registered resolvers
+
+You can now check out your GraphQL API playground at [/api/graphql](http://localhost:3000/api/graphql)
+
+
+
+
+
+### Universal Data Fetching
+
+The easiest way to create a fetcher is to use the `bridgedFetcher()` helper:
+
+```ts {3} /bridgedFetcher/3 filename="updatePost.query.ts"
+import { updatePostBridge } from './updatePost.bridge'
+// ☝️ Reuse your data bridge
+import { bridgedFetcher } from '@green-stack/schemas/bridgedFetcher'
+// ☝️ Universal graphql fetcher that can be used in any JS environment
+
+/* --- updatePostFetcher() --------- */
+
+export const updatePostFetcher = bridgedFetcher(updatePostBridge)
+```
+
+This will automatically build the query string with all relevant fields from the bridge.
+
+To write a custom query with only certain fields, you can use our `graphql()` helper *with* `bridgedFetcher()`:
+
+```ts {3, 11, 21, 31, 41} filename="updatePost.query.ts"
+import { ResultOf, VariablesOf } from 'gql.tada'
+// ☝️ Type helpers that interface with the GraphQL schema
+import { graphql } from '../graphql/graphql'
+// ☝️ Custom gql.tada query builder that integrates with our types
+import { bridgedFetcher } from '@green-stack/schemas/bridgedFetcher'
+// ☝️ Universal graphql fetcher that can be used in any JS environment
+
+/* --- Query ----------------------- */
+
+// VSCode and gql.tada will help suggest or autocomplete thanks to our schema definitions
+export const updatePostQuery = graphql(`
+ query updatePost ($updatePostArgs: UpdatePostInput) {
+ updatePost(args: $updatePostArgs) {
+ slug
+ title
+ body
+ }
+ }
+`)
+
+// ⬇⬇⬇ automatically typed as ⬇⬇⬇
+
+// TadaDocumentNode<{
+// updatePost(args: Partial): {
+// slug: string | null;
+// title: boolean | null;
+// body: boolean | null;
+// };
+// }>
+
+// ⬇⬇⬇ can be turned into reusable types ⬇⬇⬇
+
+/* --- Types ----------------------- */
+
+export type UpdatePostQueryInput = VariablesOf
+
+export type UpdatePostQueryOutput = ResultOf
+
+/* --- updatePostFetcher() --------- */
+
+export const updatePostFetcher = bridgedFetcher({
+ ...updatePostBridge, // <- Reuse your data bridge ...
+ graphqlQuery: updatePostQuery, // <- ... BUT, use our custom query
+})
+```
+
+> Whether you use a custom query or not, you now have a fetcher that:
+
+- ✅ Uses the executable graphql schema serverside
+- ✅ Can be used in the browser or mobile using fetch
+
+Want to know even more? Check the [Universal Data Fetching Docs](/universal-data-fetching).
+
+
+
+### Resolver Form State
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+To further keep your API's input and form state in sync, you can link the input schema to a form state hook using `useFormState()`:
+
+```tsx copy
+import { useFormState } from '@green-stack/forms/useFormState'
+```
+
+```tsx {10} filename="useUpdatePostFormState.ts"
+// Create a set of form state utils to use in your components
+const formState = useFormState(updatePostBridge.inputSchema, {
+ initialValues: { ... },
+ // ... other options
+})
+```
+
+More about this in the [Form Management Docs](/form-management).
+
+
diff --git a/apps/docs/pages/@green-stack-core/schemas/createGraphResolver.mdx b/apps/docs/pages/@green-stack-core/schemas/createGraphResolver.mdx
new file mode 100644
index 0000000..60a0885
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/schemas/createGraphResolver.mdx
@@ -0,0 +1,118 @@
+import { Image } from '@app/primitives'
+import { FileTree } from 'nextra/components'
+import { TitleWrapper, Hidden } from '@app/docs/components/Hidden'
+
+
+ ## `createGraphResolver()`
+
+
+
+
+# `createGraphResolver()`
+
+```typescript copy
+import { createGraphResolver } from '@green-stack/schemas/createGraphResolver'
+```
+
+
+
+
+
+
+
+
+
+
+
+We made it quite easy to enable GraphQL for your resolvers. The flow is quite similar to [adding API routes](/@green-stack-core/schemas/createNextRouteHandler).
+
+*From a related route file*, add the following:
+
+
+ ### Registering a GraphQL resolver
+
+
+```ts {3, 11} filename="features / @app-core / routes / api / health / route.ts"
+import { updatePost } from '@app/resolvers/updatePost.resolver'
+import { createNextRouteHandler } from '@green-stack/schemas/createNextRouteHandler'
+import { createGraphResolver } from '@green-stack/schemas/createGraphResolver'
+
+/* --- Routes ------------ */
+
+// exports of `GET` / `POST` / `PUT` / ...
+
+/* --- GraphQL ----------- */
+
+export const graphResolver = createGraphResolver(updatePost)
+// Automatically extracts input (☝️) from graphql request context
+```
+
+After exporting `graphResolver` here, restart the dev server or run `npm run build:schema` manually.
+
+This will:
+- 1. pick up the `graphResolver` export
+- 2. put it in our list of graphql compatible resolvers at `resolvers.generated.ts` in `@app/registries`
+- 3. recreate `schema.graphql` from input & output schemas from registered resolvers
+
+You can now check out your GraphQL API playground at [/api/graphql](http://localhost:3000/api/graphql)
+
+
+
+
+
+## Related Automations
+
+### `collect-resolvers` - script
+
+```bash copy
+npm run collect:resolvers
+```
+
+This script will:
+
+- 1. Scan the `/routes/` folder in any of your non-app workspaces
+- 2. Find all `route.ts` files that export a `createGraphResolver()` function
+- 3. Re-export them in the `@app/registries/resolvers.generated.ts` file
+
+The goal of this script is so you can define which resolvers should be registered for GraphQL on a feature by feature basis, instead of manually linking them somewhere centrally.
+
+Overall, this will keep your features more modular and portable between GREEN stack / FullProduct.dev projects.
+
+The [`collect-routes`](/@green-stack-core/scripts/collect-routes) script runs automatically on `npm run dev` and any production build commands.
+
+
+
+### `build-schema` - script
+
+```bash copy
+npm run build:schema
+```
+
+This script will:
+
+- 1. Import all the resolvers from your `@app/registries/resolvers.generated.ts` file
+- 2. Read the input and output schemas from each resolver (the [Data Bridge](/@green-stack-core/schemas/createDataBridge) for the resolver logic)
+- 3. Generate a new `schema.graphql` file in `@app/core/graphql/` folder
+- 4. Bind the schema to the resolvers to create an executable schema and effortless GraphQL API
+
+Meaning you won't need to manually write your own GraphQL schema, it will be generated automatically based on your resolvers.
+
+The [`build-schema`](/@green-stack-core/scripts/build-schema) script runs automatically on `npm run dev` and any production build commands.
+
+
+
+### [`add-resolver`](/@green-stack-core/generators/add-resolver) - generator
+
+You don't need to manually create a new API route with `createGraphResolver()` every time you create a new resolver. You can use the [`add-resolver`](/@green-stack-core/generators/add-resolver) generator to create a new resolver and its API route at once:
+
+```bash copy
+npx turbo gen resolver
+```
+
+
diff --git a/apps/docs/pages/@green-stack-core/schemas/createNextRouteHandler.mdx b/apps/docs/pages/@green-stack-core/schemas/createNextRouteHandler.mdx
new file mode 100644
index 0000000..0123977
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/schemas/createNextRouteHandler.mdx
@@ -0,0 +1,97 @@
+import { Image } from '@app/primitives'
+import { FileTree } from 'nextra/components'
+import { TitleWrapper, Hidden } from '@app/docs/components/Hidden'
+
+
+ ## `createNextRouteHandler()`
+
+
+
+
+# `createNextRouteHandler()`
+
+```typescript copy
+import { createNextRouteHandler } from '@green-stack/schemas/createNextRouteHandler'
+```
+
+
+
+
+
+
+
+
+
+
+
+You can create a new API route by exporting a `GET` / `POST` / `UPDATE` / `DELETE` handler assigned to a `createNextRouteHandler()` that wraps your "bridged resolver":
+
+
+ ### Creating API routes
+
+
+```ts /createNextRouteHandler/1,3 /updatePost/4 filename="@some-feature / routes / api / posts / [slug] / update / route.ts"
+import { updatePost } from '@app/resolvers/updatePost.resolver'
+import { createNextRouteHandler } from '@green-stack/schemas/createNextRouteHandler'
+
+/* --- Routes ------------ */
+
+export const UPDATE = createNextRouteHandler(updatePost)
+// Automatically extracts (☝️) args from url / search params
+// based on the zod 'inputSchema'
+
+// If you want to support e.g. POST (👇), same deal (checks request body too)
+export const POST = createNextRouteHandler(updatePost)
+```
+
+### How it works
+
+What `createNextRouteHandler()` does under the hood:
+- 1. extract the input from the request context
+- 2. validate it
+- 3. call the resolver function with the args (and e.g. token / session / request context)
+- 4. return the output from your resolver with defaults applied
+
+> Check [Next.js Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) to understand supported exports (like `GET` or `POST`) and their options.
+
+> Restart your dev server or run `npm run link:routes` to make sure your new API route is available.
+
+
+
+## Related Automations
+
+### `link-routes` - script
+
+```bash copy
+npm run link:routes
+```
+
+This script will:
+
+- 1. Scan the `/routes/` folder in any of your non-app workspaces
+- 2. Find all `route.ts` files that export a `createNextRouteHandler()` function
+- 3. Re-export your `GET` / `POST` / `PUT` / `DELETE` route handlers to the `@app/next` workspace
+
+The goal of this script is so you can define your API routes colocated by feature, instead of grouping by the "API route" type.
+
+Overall, this will keep your features more modular and portable between GREEN stack / FullProduct.dev projects.
+
+The [`link-routes`](/@green-stack-core/scripts/link-routes) script runs automatically on `npm run dev` and any production build commands.
+
+
+
+### [`add-resolver`](/@green-stack-core/generators/add-resolver) - generator
+
+You don't need to manually create a new API route with `createNextRouteHandler()` every time you create a new resolver. You can use the [`add-resolver`](/@green-stack-core/generators/add-resolver) generator to create a new resolver and its API route at once:
+
+```bash copy
+npx turbo gen resolver
+```
+
+
diff --git a/apps/docs/pages/@green-stack-core/schemas/createResolver.mdx b/apps/docs/pages/@green-stack-core/schemas/createResolver.mdx
new file mode 100644
index 0000000..4d01f93
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/schemas/createResolver.mdx
@@ -0,0 +1,101 @@
+import { Image } from '@app/primitives'
+import { FileTree } from 'nextra/components'
+import { TitleWrapper, Hidden } from '@app/docs/components/Hidden'
+
+
+ ## `createResolver()`
+
+
+
+
+# `createResolver()`
+
+```typescript copy
+import { createResolver } from '@green-stack/schemas/createResolver'
+```
+
+
+
+
+
+
+
+
+
+
+
+The `createResolver()` function is a utility that simplifies the creation of data resolvers from [Data Bridges](/@green-stack-core/schemas/createDataBridge):
+
+
+ ### Creating a resolver
+
+
+```typescript copy
+/* --- someResolver() --- */
+/* -i- ... */
+export const someResolver = createResolver(async ({
+ args, // <- Auto inferred types (from bridge 'inputSchema')
+ context, // <- Request context (from next.js middleware)
+ parseArgs, // <- Input validator (from bridge 'inputSchema')
+ withDefaults, // <- Response helper (from bridge 'outputSchema')
+}) => {
+
+ // Validate input and apply defaults
+ const validatedArgs = parseArgs(args)
+
+ // -- Business Logic --
+
+ // ... Your back-end business logic goes here ...
+
+ // -- Respond --
+
+ // Typecheck response and apply defaults from bridge's outputSchema
+ return withDefaults({
+ ...,
+ })
+
+}, someDataBridge)
+```
+
+> Pass the bridge (☝️) as the second argument to `createResolver()` to:
+- 1️⃣ infer the input / arg types from the bridge's `inputSchema`
+- 2️⃣ enable `parseArgs()` and `withDefaults()` helpers for validation, hints + defaults
+
+> **The resulting function can be used as just another async function** anywhere in your back-end.
+
+> The difference with a regular function, since the logic is bundled together with its data-bridge / input + output metadata, is that we can easily transform it into APIs
+
+
+
+## Why Resolvers?
+
+Our definition of a resolver is a function that:
+
+- **Takes arguments** (input data)
+- **Returns a value** (output data)
+- **Can be used as an API** (e.g. in a Next.js route handler or GraphQL API)
+- **Has its metadata like the input and output schema bundled wiht it** ([Data Bridges](/@green-stack-core/schemas/createDataBridge))
+
+That last part is important, because when you bundle the business logic with it's input and output metadata, you can easilyt transform it into APIs, while still keeping it as reusable function returning a promise.
+
+More about this in the [Data Bridges](/@green-stack-core/schemas/createDataBridge) docs.
+
+
+
+## Related Automations
+
+### [`add-resolver`](/@green-stack-core/generators/add-resolver) - generator
+
+You don't need to manually create a new resolver with `createResolver()`. You can use the [`add-resolver`](/@green-stack-core/generators/add-resolver) generator to create a new resolver and all related files (like the [DataBridge](/@green-stack-core/schemas/createDataBridge) and [API route](/@green-stack-core/schemas/createNextRouteHandler)) in one go:
+
+```bash copy
+npx turbo gen resolver
+```
+
+
diff --git a/apps/docs/pages/@green-stack-core/scripts/helpers/_meta.ts b/apps/docs/pages/@green-stack-core/scripts/helpers/_meta.ts
new file mode 100644
index 0000000..4b08fb5
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/scripts/helpers/_meta.ts
@@ -0,0 +1,4 @@
+
+export default {
+ 'scriptUtils': 'scriptUtils',
+}
diff --git a/apps/docs/pages/@green-stack-core/scripts/helpers/scriptUtils.mdx b/apps/docs/pages/@green-stack-core/scripts/helpers/scriptUtils.mdx
new file mode 100644
index 0000000..c6af135
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/scripts/helpers/scriptUtils.mdx
@@ -0,0 +1,471 @@
+import { Image } from '@app/primitives'
+import { FileTree } from 'nextra/components'
+import { TitleWrapper, Hidden } from '@app/docs/components/Hidden'
+
+
+ ## Script Utils
+
+
+
+
+# Script Utils
+
+```typescript copy
+import * as scriptUtils from '@green-stack/scripts/helpers/scriptUtils'
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
+A collection of utility functions for working with scripts, file paths, and workspace management in the Green Stack ecosystem.
+
+## File System Utilities
+
+### `excludeDirs()`
+
+Filter function to exclude folders and directories. Returns true if the path contains a file (has an extension).
+
+```typescript
+excludeDirs('path/to/file.ts') // => true
+excludeDirs('path/to/folder') // => false
+
+// Usage as a filter function:
+
+const paths = [
+ '/components/Button.tsx',
+ '/components/Input.tsx',
+ '/components/',
+ '/schemas'
+]
+
+const filteredPaths = paths.filter(excludeDirs)
+// => ['/components/Button.tsx', '/components/Input.tsx']
+```
+
+### `excludeModules()`
+
+Filter function to exclude node_modules folders. Returns true if the path does not include 'node_modules'.
+
+```typescript
+excludeModules('src/components/Button.tsx') // => true
+excludeModules('node_modules/react/index.js') // => false
+
+// Usage as a filter function:
+
+const paths = [
+ 'src/components/Button.tsx',
+ 'src/components/Input.tsx',
+ 'node_modules/react/index.js',
+ 'node_modules/@app-core/utils.js'
+]
+
+const filteredPaths = paths.filter(excludeModules)
+// => ['src/components/Button.tsx', 'src/components/Input.tsx']
+```
+
+### `globRel()`
+
+Gets the relative glob path of a glob-style selector using [`globSync`](https://github.com/isaacs/node-glob?tab=readme-ov-file#globsyncpattern-string--string-options-globoptions--string--path). The path must contain one of the workspace folder locations ('features', 'packages', or 'apps').
+
+```typescript
+globRel('packages/**/*.ts')
+// => ['packages/@green-stack-core/utils.ts', 'packages/@app-core/components.ts']
+
+globRel('features/**/components/*.tsx')
+// => ['features/@app-core/components/Button.tsx', 'features/@app-core/components/Input.tsx']
+```
+
+### `hasOptOutPatterns()`
+
+Checks if file content has opt-out patterns to determine if the file should be ignored.
+
+```typescript
+const content = `
+export const optOut = true
+// Rest of the file...
+`
+hasOptOutPatterns(content) // => true
+
+const content2 = `
+// @automation optOut = true
+// Rest of the file...
+`
+hasOptOutPatterns(content2) // => true
+```
+
+
+
+## String Utilities
+
+### `normalizeName()`
+
+Makes sure only lowercase and uppercase letters are left in a given string by removing all other characters.
+
+```typescript
+normalizeName('user-profile') // => 'userprofile'
+normalizeName('User_Profile_123') // => 'UserProfile'
+```
+
+### `matchMethods()`
+
+Checks that a certain method (like 'GET', 'POST', etc.) is included in a list of method names.
+
+```typescript
+const methods = ['GET', 'POST', 'PUT']
+matchMethods(methods, 'GET') // => true
+matchMethods(methods, 'DELETE') // => false
+```
+
+### `createDivider()`
+
+Creates a code divider that's always 100 chars wide. Can be used for documentation or code comments.
+
+```typescript
+createDivider('Section Title')
+// => '/* --- Section Title ----------------------------------------------------------------------------------- */'
+
+createDivider('Section Title', true)
+// => '/** --- Section Title ----------------------------------------------------------------------------------- */'
+```
+
+### `validateNonEmptyNoSpaces()`
+
+Validates that a string is not empty and does not contain spaces.
+
+```typescript
+validateNonEmptyNoSpaces('validName') // => true
+validateNonEmptyNoSpaces('') // => 'Please enter a non-empty value'
+validateNonEmptyNoSpaces('invalid name') // => 'Please enter a value without spaces'
+```
+
+> Handy for plop prompt input validation.
+
+
+
+## Workspace Management
+
+### `parseWorkspaces()`
+
+Figure out all info about all workspaces in the monorepo and return mapped linking info for use in scripts and generators.
+
+```typescript
+const workspaceInfo = parseWorkspaces(
+ folderLevel = '../../',
+ includeApps = false,
+)
+// => {
+//
+// /** -i- Map of { [path]: package.json config, ... } */
+// workspaceConfigs: { ... },
+//
+// /** -i- Map of { [path]: pkgName, ... } */
+// workspaceImports: { ... },
+//
+// /** -i- Map of { [pkgName]: path, ... } */
+// workspacePathsMap: { ... },
+//
+// /** -i- Array of all workspace packages, e.g. ['features/@app-core', 'packages/@green-stack-core', ...] */
+// workspacePaths: [ ... ],
+//
+// /** -i- Array of all workspace packages, e.g. ['@app/core', '@green-stack/core', ...] */
+// workspacePackages: [ ... ]
+//
+// // -- Aliases and Constants --
+//
+// /** -i- Map of { [path]: package.json config, ... } */
+// PATH_CONFIGS: { ... },
+//
+// /** -i- Map of { [pkgName]: package.json config, ... } */
+// PKG_CONFIGS: { ... },
+//
+// /** -i- Map of { [path]: pkgName, ... } */
+// PATH_PKGS: { ... },
+//
+// /** -i- Map of { [pkgName]: path, ... } */
+// PKG_PATHS: { ... },
+//
+// }
+```
+
+### `getWorkspaceOptions()`
+
+List all the available workspaces for generators to use.
+
+```typescript
+const options = getWorkspaceOptions(folderLevel = '../../', {
+ filter: ['@app'], // string[]
+ exclude = [], // string[] -i- (List of packages to exclude)
+ excludePackages = false, // boolean -i- (Exclude workspaces from /packages/?)
+ includeApps = false, // boolean -i- (Include workspaces from /apps/?)
+ includeGreenStack = false, // boolean -i- (Include /packages/@green-stack-core/?)
+ skipFormatting = false, // boolean -i- (Skip formatting the output)
+ prefer = [], // string[] -i- (Puts these options at the top of the list)
+})
+// => {
+// '@app/core -- from features/@app-core': 'features/@app-core',
+// '@app/ui -- from features/@app-ui': 'features/@app-ui'
+// }
+```
+
+### `getAvailableSchemas()`
+
+List all the available schemas in the codebase that generators can use.
+
+```typescript
+const schemas = getAvailableSchemas(folderLevel = '../../', {
+ schemaKey = 'schemaName', // 'schemaName' | 'schemaPath' | 'schemaOption'
+ includeOptOut = false, // boolean
+})
+// => {
+// 'UserSchema': {
+// schemaPath: 'features/@app-core/schemas/User.ts',
+// schemaName: 'UserSchema',
+// schemaFileName: 'User.ts',
+// schemaOption: '@app/core - User (♻ - Schema)',
+// workspacePath: 'features/@app-core',
+// workspaceName: '@app/core',
+// isNamedExport: true,
+// isDefaultExport: false,
+// schema?: ZodSchema | undefined
+// },
+// ... (more schemas)
+// }
+```
+
+The default arguments for `folderLevel` and `schemaKey` are `'../../'` and `'schemaName'`, respectively. You can adjust these based on your project structure. If you want the resulting lookup object to be keyed by either the path or option, you can pass `schemaPath` or `schemaOption` as the `schemaKey`.
+
+### `getAvailableDataBridges()`
+
+List all the available data bridges for generators to use.
+
+```typescript
+const bridges = getAvailableDataBridges('../../', {
+ filterType?: 'query', // 'query' | 'mutation'
+ bridgeKey?: 'bridgeName', // 'bridgeName' | 'bridgePath' | 'bridgeOption' | 'bridgeInputOption'
+ allowNonGraphql?: false, // boolean
+ includeOptOut?: false, // boolean
+})
+// => {
+// 'UserBridge': {
+// bridgePath: 'features/@app-core/resolvers/healthCheck.bridge.ts',
+// bridgeName: 'healthCheckBridge',
+// bridgeOption: '@app/core >>> healthCheck() (♻ - Resolver)',
+// bridgeInputOption: '@app/core >>> healthCheck() (♻ - Input)',
+// workspacePath: 'features/@app-core',
+// workspaceName: '@app/core',
+// resolverName: 'healthCheck',
+// resolverType: 'query' | 'mutation',
+// operationType: 'search' | 'add' | 'update' | 'delete' | 'get' | 'list',
+// fetcherName: 'healthCheckQuery',
+// inputSchemaName: 'HealthCheckInput',
+// outputSchemaName: 'HealthCheckOutput',
+// allowedMethods: ['GET', 'POST'],
+// isNamedExport: true,
+// isDefaultExport: false,
+// isMutation: false,
+// isQuery: true,
+// bridge?: DataBridgeType | undefined,
+// },
+// ... (more bridges)
+// }
+```
+
+
+
+## Import Management
+
+### `readImportAliases()`
+
+Retrieve the import aliases from the main tsconfig.json in `@app/core`.
+
+```typescript
+const aliases = readImportAliases('../../')
+// => {
+// '@app/core/appConfig': '@app/config',
+// '@app/core/schemas': '@app/schemas',
+// '@app/core/utils': '@app/utils',
+// '@app/core/hooks': '@app/hooks',
+// '@app/core/components/styled': '@app/primitives',
+// '@app/core/components': '@app/components',
+// '@app/core/forms': '@app/forms',
+// '@app/core/screens': '@app/screens',
+// '@app/core/assets': '@app/assets',
+// '@app/core/resolvers': '@app/resolvers',
+// '@app/core/middleware': '@app/middleware',
+// '@green-stack/core/schemas': '@green-stack/schemas',
+// '@green-stack/core/navigation/index': '@green-stack/navigation',
+// '@green-stack/core/navigation': '@green-stack/navigation',
+// '@green-stack/core/utils': '@green-stack/utils',
+// '@green-stack/core/hooks': '@green-stack/hooks',
+// '@green-stack/core/components': '@green-stack/components',
+// '@green-stack/core/styles/index': '@green-stack/styles',
+// '@green-stack/core/styles': '@green-stack/styles',
+// '@green-stack/core/forms': '@green-stack/forms',
+// '@green-stack/core/context': '@green-stack/context',
+// '@green-stack/core/scripts': '@green-stack/scripts',
+// '@green-stack/core/svg/svg.primitives': '@green-stack/svg',
+// }
+```
+
+### `swapImportAlias()`
+
+Swap an import path with an alias if a match occurs.
+
+```typescript
+swapImportAlias('@app/core/appConfig')
+// => '@app/config'
+
+swapImportAlias('@app/config')
+// => '@app/config'
+```
+
+
+
+## CLI Utilities
+
+### `opt()`
+
+Format a CLI option with proper styling and hierarchy.
+
+Will grey out the anything after the `--` when displayed in the terminal.
+
+```typescript
+opt('Some Option')
+// => 'Some Option' (nothing greyed out using ansi colors)
+
+opt('Some Option -- This extra info is greyed out')
+// => 'Some option -- This extra info is greyed out'
+```
+
+
+
+## Helper Functions
+
+### `includesOption()`
+
+Higher-order function to prefill a list of options that are checked against in the actual filter method.
+
+```typescript
+const includesGetOrPost = includesOption(['GET', 'POST'])
+const methods = ['GET', 'POST', 'PUT']
+methods.filter(includesGetOrPost) // => ['GET', 'POST']
+```
+
+### `maybeImport()`
+
+Attempts to `require()` a file, but returns an empty object if it fails or the file doesn't exist.
+
+```typescript
+// File exists
+maybeImport('./config.js')
+// => { port: 3000, host: 'localhost' }
+
+// File doesn't exist
+maybeImport('./nonexistent.js')
+// => {}
+
+// With error logging
+maybeImport('./config.js', 'logErrors')
+// => Logs error if file doesn't exist
+```
+
+
+
+## Types
+
+### `SchemaFileMeta`
+
+Metadata about a schema file:
+
+```typescript
+type SchemaFileMeta = {
+ schema?: ZodSchema | {}
+ schemaPath: string
+ schemaName: string
+ schemaFileName: string
+ schemaOption: string
+ workspacePath: string
+ workspaceName: string
+ isNamedExport: boolean
+ isDefaultExport: boolean
+}
+```
+
+### `FetcherFileMeta`
+
+Metadata about a fetcher file:
+
+```typescript
+type FetcherFileMeta = {
+ resolverName: string
+ fetcherName: string
+ fetcherType: 'query' | 'mutation'
+}
+```
+
+### `BridgeFileMeta`
+
+Metadata about a data bridge file:
+
+```typescript
+type BridgeFileMeta = {
+ bridge?: DataBridgeType | {}
+ bridgePath: string
+ bridgeName: string
+ bridgeOption: string
+ bridgeInputOption: string
+ workspacePath: string
+ workspaceName: string
+ resolverName: string
+ resolverType: 'query' | 'mutation'
+ operationType: 'add' | 'update' | 'delete' | 'get' | 'list' | 'search'
+ fetcherName: string
+ inputSchemaName: string
+ outputSchemaName: string
+ allowedMethods: ALLOWED_METHODS[]
+ isNamedExport: boolean
+ isDefaultExport: boolean
+ isMutation: boolean
+ isQuery: boolean
+}
+```
+
+## Re-exports
+
+### Utils from `stringUtils`
+
+Re-exports all the utility functions from `@green-stack/core/utils/stringUtils`:
+
+- [`snakeToCamel()`](/@green-stack-core/utils/stringUtils#snaketocamel)
+- [`snakeToDash()`](/@green-stack-core/utils/stringUtils#snaketodash)
+- [`dashToCamel()`](/@green-stack-core/utils/stringUtils#dashtocamel)
+- [`dashToSnake()`](/@green-stack-core/utils/stringUtils#dashtosnake)
+- [`camelToSnake()`](/@green-stack-core/utils/stringUtils#cameltosnake)
+
+- [`uppercaseFirstChar()`](/@green-stack-core/utils/stringUtils#uppercasefirstchar)
+- [`lowercaseFirstChar()`](/@green-stack-core/utils/stringUtils#lowercasefirstchar)
+
+- [`slugify()`](/@green-stack-core/utils/stringUtils#slugify)
+- [`replaceStringVars()`](/@green-stack-core/utils/stringUtils#replacestringvars)
+- [`replaceMany()`](/@green-stack-core/utils/stringUtils#replacemany)
+
+- [`includesAny()`](/@green-stack-core/utils/stringUtils#includesany)
+- [`extractPathParams()`](/@green-stack-core/utils/stringUtils#extractpathparams)
+
+- [`ansi` - constants](/@green-stack-core/utils/stringUtils#ansi---constants)
+- [`a` - terminal formatters](/@green-stack-core/utils/stringUtils#a---terminal-formatters)
diff --git a/apps/docs/pages/@green-stack-core/utils/_meta.ts b/apps/docs/pages/@green-stack-core/utils/_meta.ts
new file mode 100644
index 0000000..811e48f
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/utils/_meta.ts
@@ -0,0 +1,11 @@
+
+export default {
+ 'styleUtils': 'styleUtils',
+ 'stringUtils': 'stringUtils',
+ 'objectUtils': 'objectUtils',
+ 'numberUtils': 'numberUtils',
+ 'functionUtils': 'functionUtils',
+ 'commonUtils': 'commonUtils',
+ 'arrayUtils': 'arrayUtils',
+ 'apiUtils': 'apiUtils',
+}
diff --git a/apps/docs/pages/@green-stack-core/utils/apiUtils.mdx b/apps/docs/pages/@green-stack-core/utils/apiUtils.mdx
new file mode 100644
index 0000000..71ccfaf
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/utils/apiUtils.mdx
@@ -0,0 +1,226 @@
+import { Image } from '@app/primitives'
+import { FileTree } from 'nextra/components'
+import { TitleWrapper, Hidden } from '@app/docs/components/Hidden'
+
+
+ ## API Utils
+
+
+
+
+# API Utils
+
+```typescript copy
+import * as apiUtils from '@green-stack/utils/apiUtils'
+```
+
+Collection of utility functions for handling API requests, parameters, and security:
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Disclaimer - Server Only
+
+> These utilities are designed for server-side use only. They are not intended for client-side execution and may not work correctly in a browser environment.
+
+
+
+## JSON Utils
+
+### `validateJSON()`
+
+Checks whether a string is valid JSON.
+
+```typescript
+validateJSON('{"name": "John"}') // => true
+validateJSON('{"name": John}') // => false (missing quotes)
+validateJSON('not json') // => false
+```
+
+### `parseIfJSON()`
+
+Attempts to parse a string if it's valid JSON, otherwise returns the original value.
+
+```typescript
+parseIfJSON('{"name": "John"}') // => { name: 'John' }
+parseIfJSON('not json') // => 'not json'
+parseIfJSON({ already: 'object' }) // => { already: 'object' }
+```
+
+
+
+## API Parameter Utils
+
+### `getApiParam()`
+
+Gets a specific property by key from supplied API sources (query, params, body, args, context).
+
+```typescript
+const apiSources = {
+ query: { id: '123' },
+ body: { name: 'John' },
+ params: { type: 'user' }
+}
+
+getApiParam('id', apiSources) // => '123'
+getApiParam('name', apiSources) // => 'John'
+getApiParam('type', apiSources) // => 'user'
+getApiParam('nonexistent', apiSources) // => undefined
+```
+
+### `getApiParams()`
+
+Get multiple API parameters from supplied API sources.
+
+```typescript
+const apiSources = {
+ query: { id: '123' },
+ body: { name: 'John' },
+ params: { type: 'user' }
+}
+
+getApiParams(['id', 'name'], apiSources)
+// => { id: '123', name: 'John' }
+
+getApiParams('id name type', apiSources)
+// => { id: '123', name: 'John', type: 'user' }
+```
+
+### `getUrlParams()`
+
+Extracts and parses query parameters from a URL.
+
+```typescript
+getUrlParams('https://api.example.com/users?id=123&name=John')
+// => { id: 123, name: 'John' }
+
+getUrlParams('https://api.example.com/users')
+// => {}
+```
+
+
+
+## Security Utils
+
+### `createHmac()`
+
+Creates a cryptographic signature based on the data and chosen algorithm.
+
+```typescript
+createHmac('sensitive-data', 'sha256')
+// => 'hashed-signature'
+
+createHmac('sensitive-data', 'md5')
+// => 'hashed-signature'
+```
+
+### `encrypt()`
+
+Encrypts data using AES encryption with a secret key.
+
+```typescript
+const encrypted = encrypt('sensitive-data', 'my-secret-key')
+// => 'encrypted-string'
+
+// Using APP_SECRET environment variable
+const encrypted = encrypt('sensitive-data')
+// => 'encrypted-string'
+```
+
+### `decrypt()`
+
+Decrypts data that was encrypted using the `encrypt()` function.
+
+```typescript
+const decrypted = decrypt(encrypted, 'my-secret-key')
+// => 'sensitive-data'
+
+// Using APP_SECRET environment variable
+const decrypted = decrypt(encrypted)
+// => 'sensitive-data'
+```
+
+
+
+## Header Context Utils
+
+### `createMiddlewareContext()`
+
+Adds serializable context data to request headers (signed based on APP_SECRET).
+
+```typescript
+const headers = await createMiddlewareContext(
+ request,
+ { userId: '123', role: 'admin' },
+ { 'X-Custom-Header': 'value' }
+)
+// => Headers object with context and custom headers
+```
+
+### `getHeaderContext()`
+
+Extracts and validates header context from various request types.
+
+```typescript
+// From NextApiRequest
+const context = getHeaderContext(nextApiRequest)
+// => { userId: '123', role: 'admin' }
+
+// From standard Request
+const context = getHeaderContext(request)
+// => { userId: '123', role: 'admin' }
+
+// From GetServerSidePropsContext
+const context = getHeaderContext(context.req)
+// => { userId: '123', role: 'admin' }
+```
+
+Note: Both header context utilities require the `APP_SECRET` environment variable to be set for signing and validation.
+
+
+
+## Fire and Forget Utils
+
+### `fireGetAndForget()`
+
+Fires a GET request and ignores whether it succeeds or not (useful for webhooks).
+
+```typescript
+fireGetAndForget('https://api.example.com/webhook')
+// => true (immediately, doesn't wait for response)
+
+// With custom config
+fireGetAndForget('https://api.example.com/webhook', {
+ headers: { 'X-API-Key': 'secret' }
+})
+```
+
+### `firePostAndForget()`
+
+Fires a POST request and ignores whether it succeeds or not (useful for webhooks).
+
+```typescript
+firePostAndForget('https://api.example.com/webhook', { event: 'user.created' })
+// => true (immediately, doesn't wait for response)
+
+// With custom config
+firePostAndForget('https://api.example.com/webhook',
+ { event: 'user.created' },
+ { headers: { 'X-API-Key': 'secret' } }
+)
+```
diff --git a/apps/docs/pages/@green-stack-core/utils/arrayUtils.mdx b/apps/docs/pages/@green-stack-core/utils/arrayUtils.mdx
new file mode 100644
index 0000000..dabf1f3
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/utils/arrayUtils.mdx
@@ -0,0 +1,133 @@
+import { Image } from '@app/primitives'
+import { FileTree } from 'nextra/components'
+import { TitleWrapper, Hidden } from '@app/docs/components/Hidden'
+
+
+ ## Array Utils
+
+
+
+
+# Array Utils
+
+```typescript copy
+import * as arrayUtils from '@green-stack/utils/arrayUtils'
+```
+
+Collection of utility functions for array manipulation and set-like operations:
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Set-like Operations
+
+### `arrFromSet()`
+
+Deduplicates items in an array using Set.
+
+```typescript
+const numbers = [1, 2, 2, 3, 3, 3]
+arrFromSet(numbers) // => [1, 2, 3]
+
+const strings = ['a', 'b', 'a', 'c']
+arrFromSet(strings) // => ['a', 'b', 'c']
+
+// Works with objects too
+const objects = [{ id: 1 }, { id: 1 }, { id: 2 }]
+arrFromSet(objects) // => [{ id: 1 }, { id: 1 }, { id: 2 }]
+```
+
+### `addSetItem()`
+
+Adds an item to array if it doesn't exist already. Uses JSON.stringify for deep comparison.
+
+```typescript
+const arr = [{ id: 1 }, { id: 2 }]
+
+// Adding a new item
+addSetItem(arr, { id: 3 })
+// => [{ id: 1 }, { id: 2 }, { id: 3 }]
+
+// Adding a duplicate (won't add)
+addSetItem(arr, { id: 1 })
+// => [{ id: 1 }, { id: 2 }, { id: 3 }]
+```
+
+### `removeSetItem()`
+
+Removes an item from an array.
+
+```typescript
+const arr = [1, 2, 3, 4]
+
+removeSetItem(arr, 3) // => [1, 2, 4]
+removeSetItem(arr, 5) // => [1, 2, 3, 4] (no change if item not found)
+```
+
+### `toggleArrayItem()`
+
+Adds or removes an item from an array (toggles its presence).
+
+```typescript
+const arr = [1, 2, 3]
+
+// Adding an item
+toggleArrayItem(arr, 4) // => [1, 2, 3, 4]
+
+// Removing an item
+toggleArrayItem(arr, 2) // => [1, 3]
+```
+
+
+
+## Array Transformation
+
+### `createLookup()`
+
+Creates a lookup object from an array of objects, indexed by a specified property key.
+
+```typescript
+const users = [
+ { id: 1, name: 'John' },
+ { id: 2, name: 'Jane' },
+ { id: 3, name: 'Bob' }
+]
+
+// Create a lookup by id
+const userLookup = createLookup(users, 'id')
+// => {
+// 1: { id: 1, name: 'John' },
+// 2: { id: 2, name: 'Jane' },
+// 3: { id: 3, name: 'Bob' }
+// }
+
+// Access user by id
+userLookup[1] // => { id: 1, name: 'John' }
+
+// Items without the key are skipped
+const mixedData = [
+ { id: 1, name: 'John' },
+ { name: 'Jane' }, // no id, will be skipped
+ { id: 3, name: 'Bob' }
+]
+const lookup = createLookup(mixedData, 'id')
+// => {
+// 1: { id: 1, name: 'John' },
+// 3: { id: 3, name: 'Bob' }
+// }
+```
\ No newline at end of file
diff --git a/apps/docs/pages/@green-stack-core/utils/commonUtils.mdx b/apps/docs/pages/@green-stack-core/utils/commonUtils.mdx
new file mode 100644
index 0000000..5861139
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/utils/commonUtils.mdx
@@ -0,0 +1,126 @@
+import { Image } from '@app/primitives'
+import { FileTree } from 'nextra/components'
+import { TitleWrapper, Hidden } from '@app/docs/components/Hidden'
+
+
+ ## Common Utils
+
+
+
+
+# Common Utils
+
+```typescript copy
+import * as commonUtils from '@green-stack/utils/commonUtils'
+```
+
+Collection of common utility functions for general use:
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Validation Utils
+
+### `isEmpty()`
+
+Checks for null, undefined, empty strings, empty objects or empty arrays.
+
+```typescript
+// Empty values
+isEmpty(null) // => true
+isEmpty(undefined) // => true
+isEmpty('') // => true
+isEmpty([]) // => true
+isEmpty({}) // => true
+
+// Non-empty values
+isEmpty('hello') // => false
+isEmpty([1, 2, 3]) // => false
+isEmpty({ key: 'value' }) // => false
+
+// Custom behavior for empty strings
+isEmpty('', false) // => false (when failOnEmptyStrings is false)
+```
+
+### `isKvRecord()`
+
+Checks whether an object is a simple key-value record (all values are primitives).
+
+```typescript
+// Valid key-value records
+isKvRecord({ name: 'John', age: 30 }) // => true
+isKvRecord({ active: true, count: 0 }) // => true
+isKvRecord({ title: undefined, id: null }) // => true
+
+// Invalid key-value records
+isKvRecord({ data: [] }) // => false (array value)
+isKvRecord({ handler: () => {} }) // => false (function value)
+isKvRecord({ user: { name: 'John' } }) // => false (nested object)
+isKvRecord([]) // => false (array)
+isKvRecord(null) // => false (null)
+```
+
+
+
+## Console Utils
+
+These utilities help with logging messages only once, which is useful for one-off messages, warnings, or errors.
+
+### `consoleOnce()`
+
+Base function that logs to the console only once, skip on subsequent logs.
+
+```typescript
+consoleOnce('This will only show once', console.log, 'Additional data')
+consoleOnce('This will only show once', console.log, 'Additional data') // Won't show
+```
+
+### `logOnce()`
+
+Logs a message to the console only once.
+
+```typescript
+logOnce('This will only show once')
+logOnce('This will only show once') // Won't show
+```
+
+### `warnOnce()`
+
+Shows a warning message only once.
+
+```typescript
+warnOnce('This warning will only show once')
+warnOnce('This warning will only show once') // Won't show
+```
+
+### `errorOnce()`
+
+Shows an error message only once.
+
+```typescript
+errorOnce('This error will only show once')
+errorOnce('This error will only show once') // Won't show
+```
+
+All console utilities support additional arguments that will be passed to the console method:
+
+```typescript
+logOnce('User logged in:', { userId: 123, timestamp: new Date() })
+warnOnce('Deprecated feature used:', { feature: 'oldMethod', version: '1.0.0' })
+errorOnce('API Error:', { status: 500, message: 'Internal Server Error' })
+```
diff --git a/apps/docs/pages/@green-stack-core/utils/functionUtils.mdx b/apps/docs/pages/@green-stack-core/utils/functionUtils.mdx
new file mode 100644
index 0000000..611f79a
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/utils/functionUtils.mdx
@@ -0,0 +1,95 @@
+import { Image } from '@app/primitives'
+import { FileTree } from 'nextra/components'
+import { TitleWrapper, Hidden } from '@app/docs/components/Hidden'
+
+
+ ## Function Utils
+
+
+
+
+# Function Utils
+
+```typescript copy
+import * as functionUtils from '@green-stack/utils/functionUtils'
+```
+
+Collection of utility functions for handling asynchronous operations and error handling:
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Error Handling
+
+### `tryCatch()`
+
+Attempts to execute a promise, wraps it with try/catch, and returns either the data or error in a structured way.
+
+```typescript
+// Successful promise
+const successPromise = Promise.resolve({ data: 'success' })
+const result = await tryCatch(successPromise)
+// => { data: { data: 'success' } }
+
+// Failed promise with Error
+const errorPromise = Promise.reject(new Error('Something went wrong'))
+const errorResult = await tryCatch(errorPromise)
+// => { error: Error('Something went wrong') }
+
+// Failed promise with non-Error
+const nonErrorPromise = Promise.reject('String error')
+const nonErrorResult = await tryCatch(nonErrorPromise)
+// => { error: Error('String error') }
+```
+
+Common use cases:
+
+```typescript
+// API call with error handling
+const fetchData = async () => {
+ const { data, error } = await tryCatch(fetch('/api/data'))
+
+ if (error) {
+ console.error('Failed to fetch data:', error)
+ return null
+ }
+
+ return data
+}
+
+// Database operation
+const createUser = async (userData) => {
+ const { data, error } = await tryCatch(db.users.create(userData))
+
+ if (error) {
+ // Handle specific error cases
+ if (error.message.includes('duplicate')) {
+ throw new Error('User already exists')
+ }
+ throw error
+ }
+
+ return data
+}
+```
+
+This is particularly useful when you want to:
+- Handle errors in a consistent way
+- Avoid try/catch blocks in your business logic
+- Ensure all errors are properly converted to Error objects
+- Keep error handling separate from the main logic flow
\ No newline at end of file
diff --git a/apps/docs/pages/@green-stack-core/utils/numberUtils.mdx b/apps/docs/pages/@green-stack-core/utils/numberUtils.mdx
new file mode 100644
index 0000000..2d0927f
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/utils/numberUtils.mdx
@@ -0,0 +1,138 @@
+import { Image } from '@app/primitives'
+import { FileTree } from 'nextra/components'
+import { TitleWrapper, Hidden } from '@app/docs/components/Hidden'
+
+
+ ## Number Utils
+
+
+
+
+# Number Utils
+
+```typescript copy
+import * as numberUtils from '@green-stack/utils/numberUtils'
+```
+
+Collection of utility functions for number manipulation and validation:
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Random Number Utils
+
+### `randomInt()`
+
+Generates a random integer between a max & min range.
+
+```typescript
+// Generate a random number between 0 and 10
+const result = randomInt(10) // => 7
+
+// Generate a random number between 5 and 10
+const result2 = randomInt(10, 5) // => 8
+```
+
+
+
+## Rounding Utils
+
+### `roundTo()`
+
+Uses scientific string notation to round numbers to specific decimals. Can pass a specific math rounding function as third arg.
+
+```typescript
+// Round to 2 decimal places using default rounding
+roundTo(3.14159, 2) // => 3.14
+
+// Round to 2 decimal places using ceiling
+roundTo(3.14159, 2, Math.ceil) // => 3.15
+
+// Round to 2 decimal places using floor
+roundTo(3.14159, 2, Math.floor) // => 3.14
+```
+
+### `roundUpTo()`
+
+Convenience function to round up to specific decimals.
+
+```typescript
+roundUpTo(3.14159, 2) // => 3.15
+```
+
+### `roundDownTo()`
+
+Convenience function to round down to specific decimals.
+
+```typescript
+roundDownTo(3.14159, 2) // => 3.14
+```
+
+
+
+## Number Validation Utils
+
+### `hasLeadingZeroes()`
+
+Detects leading zeroes in "numbers".
+
+```typescript
+hasLeadingZeroes('0568') // => true
+hasLeadingZeroes('568') // => false
+hasLeadingZeroes('0') // => false
+```
+
+### `isValidNumber()`
+
+Checks if a string is a valid number, excluding leading zeroes.
+
+```typescript
+// Valid numbers
+isValidNumber('0') // => true
+isValidNumber('1') // => true
+isValidNumber('250') // => true
+isValidNumber('0.123') // => true
+isValidNumber(42) // => true
+
+// Invalid numbers
+isValidNumber('0568') // => false (leading zero)
+isValidNumber('abc') // => false (not a number)
+isValidNumber('') // => false (empty string)
+isValidNumber(null) // => false (null)
+isValidNumber(undefined) // => false (undefined)
+isValidNumber(NaN) // => false (NaN)
+```
+
+### `parseIfValidNumber()`
+
+Checks if a variable can be parsed as a valid number, and then parses that number.
+
+```typescript
+// Valid numbers
+parseIfValidNumber('42') // => 42
+parseIfValidNumber('0.123') // => 0.123
+parseIfValidNumber(42) // => 42
+
+// Invalid numbers
+parseIfValidNumber('0568') // => undefined
+parseIfValidNumber('abc') // => undefined
+parseIfValidNumber('') // => undefined
+parseIfValidNumber(null) // => undefined
+parseIfValidNumber(undefined) // => undefined
+parseIfValidNumber(NaN) // => undefined
+```
\ No newline at end of file
diff --git a/apps/docs/pages/@green-stack-core/utils/objectUtils.mdx b/apps/docs/pages/@green-stack-core/utils/objectUtils.mdx
new file mode 100644
index 0000000..3223fe9
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/utils/objectUtils.mdx
@@ -0,0 +1,206 @@
+import { Image } from '@app/primitives'
+import { FileTree } from 'nextra/components'
+import { TitleWrapper, Hidden } from '@app/docs/components/Hidden'
+
+
+ ## Object Utils
+
+
+
+
+# Object Utils
+
+```typescript copy
+import * as objectUtils from '@green-stack/utils/objectUtils'
+```
+
+Collection of utility functions for object manipulation and URL parameter handling:
+
+
+
+
+
+
+
+
+
+
+
+
+
+## URL Parameter Utils
+
+### `parseUrlParamsObject()`
+
+Parses object property values, converting string representations to their proper types. Handles nested objects and arrays.
+
+```typescript
+// Basic type conversion
+parseUrlParamsObject({
+ id: '123', // => 123 (number)
+ active: 'true', // => true (boolean)
+ name: 'John' // => 'John' (string)
+})
+
+// Nested objects
+parseUrlParamsObject({
+ 'user.name': 'John',
+ 'user.age': '25'
+})
+// => { user: { name: 'John', age: 25 } }
+
+// Arrays
+parseUrlParamsObject({
+ 'items[0]': '1',
+ 'items[1]': '2'
+})
+// => { items: [1, 2] }
+
+// Ignoring specific keys
+parseUrlParamsObject({
+ id: '123',
+ raw: 'true'
+}, ['raw'])
+// => { id: 123, raw: 'true' }
+```
+
+### `buildUrlParamsObject()`
+
+Builds an object with all array and object keys flattened. Essentially the opposite of `parseUrlParamsObject()`.
+
+```typescript
+// Arrays
+buildUrlParamsObject({ arr: [0, 2] })
+// => { 'arr[0]': 0, 'arr[1]': 2 }
+
+// Nested objects
+buildUrlParamsObject({ obj: { prop: true } })
+// => { 'obj.prop': true }
+
+// Empty values are removed
+buildUrlParamsObject({
+ name: 'John',
+ empty: null,
+ nested: { value: undefined }
+})
+// => { name: 'John' }
+```
+
+
+
+## Object Transformation
+
+### `swapEntries()`
+
+Swaps the object's keys and values while keeping the types intact. Particularly useful for object enums.
+
+```typescript
+const Status = {
+ ACTIVE: 'active',
+ INACTIVE: 'inactive'
+} as const
+
+const StatusReverse = swapEntries(Status)
+// => {
+// active: 'ACTIVE',
+// inactive: 'INACTIVE'
+// }
+
+// Type-safe usage
+const status: keyof typeof Status = 'ACTIVE'
+const value: typeof Status[typeof status] = 'active'
+```
+
+### `createKey()`
+
+Turns an object into a string by deeply rendering both keys & values to string and joining them.
+
+```typescript
+// Simple object
+createKey({ id: 1, name: 'John' })
+// => 'id-1-name-John'
+
+// Nested object
+createKey({
+ user: { id: 1, name: 'John' },
+ active: true
+})
+// => 'active-true-user-id-1-name-John'
+
+// Array values
+createKey({
+ ids: [1, 2, 3],
+ tags: ['a', 'b']
+})
+// => 'ids-1-2-3-tags-a-b'
+
+// Custom separator
+createKey({ id: 1, name: 'John' }, '_')
+// => 'id_1_name_John'
+```
+
+
+
+## Dot Prop Utils
+
+The module re-exports several utilities from the `dot-prop` package for working with nested object properties:
+
+### `getProperty()`
+
+Gets a property from a nested object using dot notation.
+
+```typescript
+const obj = { user: { name: 'John' } }
+getProperty(obj, 'user.name') // => 'John'
+```
+
+### `setProperty()`
+
+Sets a property in a nested object using dot notation.
+
+```typescript
+const obj = {}
+setProperty(obj, 'user.name', 'John')
+// => { user: { name: 'John' } }
+```
+
+### `deleteProperty()`
+
+Deletes a property from a nested object using dot notation.
+
+```typescript
+const obj = { user: { name: 'John' } }
+deleteProperty(obj, 'user.name')
+// => { user: {} }
+```
+
+### `hasProperty()`
+
+Checks if a property exists in a nested object using dot notation.
+
+```typescript
+const obj = { user: { name: 'John' } }
+hasProperty(obj, 'user.name') // => true
+hasProperty(obj, 'user.age') // => false
+```
+
+### `deepKeys()`
+
+Gets all keys from a nested object, including nested properties.
+
+```typescript
+const obj = {
+ user: {
+ name: 'John',
+ address: { city: 'New York' }
+ }
+}
+deepKeys(obj)
+// => ['user.name', 'user.address.city']
+```
\ No newline at end of file
diff --git a/apps/docs/pages/@green-stack-core/utils/stringUtils.mdx b/apps/docs/pages/@green-stack-core/utils/stringUtils.mdx
new file mode 100644
index 0000000..c067016
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/utils/stringUtils.mdx
@@ -0,0 +1,231 @@
+import { Image } from '@app/primitives'
+import { FileTree } from 'nextra/components'
+import { TitleWrapper, Hidden } from '@app/docs/components/Hidden'
+
+
+ ## String Utils
+
+
+
+
+# String Utils
+
+```typescript copy
+import * as stringUtils from '@green-stack/utils/stringUtils'
+```
+
+Collection of utility functions for string manipulation and formatting:
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Case Conversion Utils
+
+### `snakeToCamel()`
+
+Converts a snake_case string to camelCase.
+
+```typescript
+const result = snakeToCamel('hello_world') // => 'helloWorld'
+```
+
+### `snakeToDash()`
+
+Converts a snake_case string to kebab-case.
+
+```typescript
+const result = snakeToDash('hello_world') // => 'hello-world'
+```
+
+### `dashToCamel()`
+
+Converts a kebab-case string to camelCase.
+
+```typescript
+const result = dashToCamel('hello-world') // => 'helloWorld'
+```
+
+### `dashToSnake()`
+
+Converts a kebab-case string to snake_case.
+
+```typescript
+const result = dashToSnake('hello-world') // => 'hello_world'
+```
+
+### `camelToSnake()`
+
+Converts a camelCase string to snake_case.
+
+```typescript
+const result = camelToSnake('helloWorld') // => 'hello_world'
+```
+
+### `camelToDash()`
+
+Converts a camelCase string to kebab-case.
+
+```typescript
+const result = camelToDash('helloWorld') // => 'hello-world'
+```
+
+
+
+## Character Case Utils
+
+### `uppercaseFirstChar()`
+
+Uppercases the first character of a string.
+
+```typescript
+const result = uppercaseFirstChar('hello') // => 'Hello'
+```
+
+### `lowercaseFirstChar()`
+
+Lowercases the first character of a string.
+
+```typescript
+const result = lowercaseFirstChar('Hello') // => 'hello'
+```
+
+
+
+## String Transformation Utils
+
+### `slugify()`
+
+Converts a string to a URL-friendly slug.
+
+```typescript
+const result = slugify('Hello World!') // => 'hello-world'
+```
+
+### `replaceStringVars()`
+
+Replaces placeholders like `{somevar}` or `[somevar]` with values from injectables.
+
+```typescript
+const result = replaceStringVars(
+ 'Hello {name}, welcome to [place]!',
+ { name: 'John', place: 'Paris' }
+) // => 'Hello John, welcome to Paris!'
+```
+
+### `replaceMany()`
+
+Replaces every string you pass as the targets with the replacement string.
+
+```typescript
+// Removes 'Update' or 'Add' from the string
+replaceMany('useUpdateDataForm()', ['Update', 'Add'], '')
+// => 'useDataForm()'
+
+replaceMany('useAddUserForm()', ['Update', 'Add'], '')
+// => 'useUserForm()'
+```
+
+## String Analysis Utils
+
+### `findTargetString()`
+
+Finds a `$target$` string inside another string.
+
+```typescript
+const folderWithIcons = findTargetString(
+ 'some/path/to/specific-folder/icons/',
+ 'some/path/to/$target$/icons/'
+) // => 'specific-folder'
+```
+
+### `includesAny()`
+
+Checks whether a given string includes any of the provided words (case-insensitive).
+
+```typescript
+const result = includesAny('Hello World', ['world', 'universe']) // => true
+```
+
+### `extractPathParams()`
+
+Extracts an array of potential params from a URL path.
+
+```typescript
+const params = extractPathParams('/api/user/[slug]/posts/[id]')
+// => ['slug', 'id']
+```
+
+
+
+## ANSI Terminal Formatting
+
+We provide two main objects for terminal text formatting:
+
+### `ansi` - constants
+
+Constants for ANSI escape codes, including:
+
+- Text styles: `reset`, `bold`, `dim`, `underscore`, etc.
+- Colors: `black`, `red`, `green`, `yellow`, etc.
+- Backgrounds: `bgBlack`, `bgRed`, `bgGreen`, etc.
+
+You should finish each ANSI code with `reset` to clear formatting for what follows after it.
+
+To automate this, you can use the `a` helper functions below.
+
+### `a` - terminal formatters
+
+Ansi helper functions for applying ANSI formatting to strings:
+
+```typescript
+// Utility
+a.bold('...') // Makes text bold
+a.underscore('...') // Underlines text
+a.reset('...') // Resets all formatting for this string + after
+a.italic('...') // Italicizes text
+
+// Colors
+a.muted('...') // Makes text muted
+a.black('...') // Black text
+a.red('...') // Red text
+a.green('...') // Green text
+a.yellow('...') // Yellow text
+a.blue('...') // Blue text
+a.magenta('...') // Magenta text
+a.cyan('...') // Cyan text
+a.white('...') // White text
+
+// Backgrounds
+a.bgBlack('...') // Black background
+a.bgRed('...') // Red background
+a.bgGreen('...') // Green background
+a.bgYellow('...') // Yellow background
+a.bgBlue('...') // Blue background
+a.bgMagenta('...') // Magenta background
+a.bgCyan('...') // Cyan background
+a.bgWhite('...') // White background
+```
+
+Each formatting function accepts:
+- `msg`: The message to format
+- `clear`: Optional boolean to reset formatting before applying new style, default is `true`.
+
+Example:
+```typescript
+console.log(a.bold(a.red('Error:')) + ' Something went wrong')
+```
diff --git a/apps/docs/pages/@green-stack-core/utils/styleUtils.mdx b/apps/docs/pages/@green-stack-core/utils/styleUtils.mdx
new file mode 100644
index 0000000..1bfc4e5
--- /dev/null
+++ b/apps/docs/pages/@green-stack-core/utils/styleUtils.mdx
@@ -0,0 +1,154 @@
+import { Image } from '@app/primitives'
+import { FileTree } from 'nextra/components'
+import { TitleWrapper, Hidden } from '@app/docs/components/Hidden'
+
+
+ ## Style Utils
+
+
+
+
+# Style Utils
+
+```typescript copy
+import * as styleUtils from '@green-stack/utils/styleUtils'
+```
+
+Collection of utility functions for handling CSS classes, theme colors, and CSS variable manipulation:
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Class Name Utils
+
+### `cn()`
+
+Combines an array of classNames but filters out falsy array elements. Uses `clsx` and `tailwind-merge` under the hood.
+
+```typescript
+// Basic usage
+cn('text-red-500', 'bg-blue-500')
+// => 'text-red-500 bg-blue-500'
+
+// With conditional classes
+cn(
+ 'base-class',
+ isActive && 'active-class',
+ isError ? 'error-class' : 'success-class'
+)
+// => 'base-class active-class error-class' (when isActive is true and isError is true)
+
+// Merging Tailwind classes
+cn('px-2 py-1 bg-red-500', 'px-4 bg-blue-500')
+// => 'py-1 px-4 bg-blue-500' (later classes override earlier ones)
+```
+
+
+
+## CSS Variable Utils
+
+### `extractCssVar()`
+
+Extracts the CSS variable name from any string if present.
+
+```typescript
+extractCssVar('var(--primary-color)') // => '--primary-color'
+extractCssVar('color: var(--text-color)') // => '--text-color'
+extractCssVar('regular-text') // => '' (no CSS variable found)
+```
+
+
+
+## Theme Color Utils
+
+### `getThemeColor()`
+
+Retrieves the nativewind theme color for the global.css variable provided.
+
+```typescript
+// Get color for current theme
+getThemeColor('primary-color')
+// => '#007AFF' (or whatever the color is in your theme)
+
+// Get color for specific theme
+getThemeColor('primary-color', 'dark')
+// => '#0A84FF' (dark theme color)
+```
+
+### `useThemeColor()`
+
+React hook that retrieves the nativewind theme color for the global.css variable provided.
+
+```typescript
+function MyComponent() {
+ const primaryColor = useThemeColor('primary-color')
+ // => '#007AFF' in light mode, '#0A84FF' in dark mode
+
+ return (
+
+ Themed Text
+
+ )
+}
+```
+
+
+
+## CSS Parsing Utils
+
+### `parseGlobalCSS()`
+
+Parses the contents of the global.css file to extract light & dark mode colors if present. Only detects colors defined within `:root` and `.dark:root`.
+
+```typescript
+const globalCSS = `
+:root {
+ --primary-color: #007AFF;
+ --secondary-color: #5856D6;
+ --text-color: #000000;
+}
+
+.dark:root {
+ --primary-color: #0A84FF;
+ --secondary-color: #5E5CE6;
+ --text-color: #FFFFFF;
+}
+`
+
+const themeColors = parseGlobalCSS(globalCSS)
+// => {
+// light: {
+// '--primary-color': '#007AFF',
+// '--secondary-color': '#5856D6',
+// '--text-color': '#000000'
+// },
+// dark: {
+// '--primary-color': '#0A84FF',
+// '--secondary-color': '#5E5CE6',
+// '--text-color': '#FFFFFF'
+// }
+// }
+```
+
+The parser supports various color formats:
+- Hex colors (`#RRGGBB` or `#RGB`)
+- RGB/RGBA colors (`rgb(r, g, b)` or `rgba(r, g, b, a)`)
+- HSL colors (`hsl(h, s%, l%)`)
+- CSS variables (`var(--variable-name)`)
+
+Note: The parser will only detect colors if they are defined within `:root` and `.dark:root` selectors in your global CSS file.
\ No newline at end of file
diff --git a/apps/docs/pages/_app.tsx b/apps/docs/pages/_app.tsx
new file mode 100644
index 0000000..2f96f9e
--- /dev/null
+++ b/apps/docs/pages/_app.tsx
@@ -0,0 +1,83 @@
+import type { AppProps } from 'next/app'
+import React, { useEffect } from 'react'
+import { useTheme } from 'nextra-theme-docs'
+import { useColorScheme } from 'nativewind'
+import UniversalAppProviders from '@app/screens/UniversalAppProviders'
+import ServerStylesProvider from '@app/next/app/ServerStylesProvider'
+import { SafeAreaProvider } from 'react-native-safe-area-context'
+import { ComponentDocsContextManager } from '@app/core/mdx/ComponentDocs'
+import { Image as NextContextImage } from '@green-stack/components/Image.next'
+import { Link as NextContextLink } from '@green-stack/navigation/Link.next'
+import { useRouter as useNextRouter } from 'next/router'
+import { useRouter as useNextContextRouter } from '@green-stack/navigation/useRouter.next'
+import { useRouteParams as useNextRouteParams } from '@green-stack/navigation/useRouteParams.next'
+import '@app/next/global.css'
+
+/* --- ---------------------------------------------------------------------------------- */
+
+export default function App({ Component, pageProps }: AppProps) {
+ // Navigation
+ const nextContextRouter = useNextContextRouter()
+ const nextRouter = useNextRouter()
+
+ // Styles
+ const theme = useTheme()
+ const scheme = useColorScheme()
+ const resolvedTheme = theme.resolvedTheme || theme.systemTheme
+
+ // -- Theme Effects --
+
+ useEffect(() => {
+ // Figure out the theme and apply it
+ const resolveTheme = () => {
+ const storedTheme = localStorage.getItem('theme')
+ let currentTheme = (resolvedTheme || storedTheme) as 'light' | 'dark' | 'system'
+ if (currentTheme === 'system' || !currentTheme) {
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
+ const prefersDark = mediaQuery.media === '(prefers-color-scheme: dark)' && mediaQuery.matches
+ currentTheme = prefersDark ? 'dark' : 'light'
+ }
+ theme.setTheme(currentTheme)
+ scheme.setColorScheme(currentTheme)
+ }
+ // Hacky, but it works 🤷♂️ (listening for theme or scheme changes is unreliable, sadly)
+ const queuedThemeCheck = () => {
+ window.scrollTo(0, 0)
+ new Array(20).fill(null).forEach((_, idx) => setTimeout(resolveTheme, idx * 200))
+ }
+ // Attach listeners
+ const $themeButtons = document.querySelectorAll('button[title="Change theme"]')
+ $themeButtons.forEach($button => $button.addEventListener('click', queuedThemeCheck))
+ // Trigger on mount or when theme changes
+ resolveTheme()
+ // Remove listeners
+ return () => $themeButtons.forEach($button => {
+ $button.removeEventListener('click', queuedThemeCheck)
+ })
+ }, [resolvedTheme])
+
+ // -- Render --
+
+ return (
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/docs/pages/_document.tsx b/apps/docs/pages/_document.tsx
new file mode 100644
index 0000000..a0e3129
--- /dev/null
+++ b/apps/docs/pages/_document.tsx
@@ -0,0 +1,52 @@
+import Document, { Html, Head, Main, NextScript } from 'next/document'
+
+/* --- Constants ------------------------------------------------------------------------------- */
+
+const isProd = process.env.NODE_ENV === 'production'
+
+/* --- -------------------------------------------------------------------------- */
+
+class AppDocument extends Document {
+ render() {
+ return (
+
+
+ {isProd && }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
+
+export default AppDocument
diff --git a/apps/docs/pages/_meta.ts b/apps/docs/pages/_meta.ts
new file mode 100644
index 0000000..19a90a9
--- /dev/null
+++ b/apps/docs/pages/_meta.ts
@@ -0,0 +1,86 @@
+import { isEmpty } from '@green-stack/utils/commonUtils'
+import { featureMeta, packageMeta } from '@app/registries/workspaceImports.generated'
+
+/* --- Top Level Sidebar ----------------------------------------------------------------------- */
+
+export const meta = {
+
+ 'index': 'Quickstart',
+ 'core-concepts': 'Core Concepts',
+ 'project-structure': 'Project Structure',
+ 'single-sources-of-truth': 'Single Sources of Truth',
+
+ // -- Building your app --
+
+ '-- Building universal apps': {
+ 'type': 'separator',
+ 'title': 'Building universal apps',
+ },
+
+ 'universal-routing': 'Cross-Platform Routing',
+ 'write-once-styles': 'Styling Universal UI',
+ 'data-resolvers': 'Flexible Resolvers and API\'s',
+ 'data-fetching': 'Universal Data Fetching',
+ 'form-management': 'Form Management',
+
+ // -- Guides --
+
+ '-- Guides': {
+ 'type': 'separator',
+ 'title': 'Guides',
+ },
+
+ 'app-config': 'Env Vars + App Config',
+
+ // -- Portability --
+
+ // '-- Portability': {
+ // 'type': 'separator',
+ // 'title': 'Portability',
+ // },
+
+ // 'maximum-code-reuse': 'Maximize Code Reuse',
+ // 'workspace-drivers': 'Workspace Drivers',
+
+ // -- Time Savers --
+
+ '-- Time Savers': {
+ 'type': 'separator',
+ 'title': 'Time Savers',
+ },
+
+ // 'automations': 'Scripts and Automations',
+ 'generators': 'Code Generators',
+ // 'git-based-plugins': 'Git based Plugins',
+
+ // -- Features --
+
+ ...(!isEmpty(featureMeta) ? {
+ '-- App Features': {
+ 'type': 'separator',
+ 'title': 'App Features',
+ },
+ } : {}),
+
+ ...featureMeta,
+
+ // -- Packages --
+
+ ...(!isEmpty(packageMeta) ? {
+ '-- Packages': {
+ 'type': 'separator',
+ 'title': 'Packages',
+ },
+ } : {}),
+
+ ...packageMeta,
+
+ // -- EndPadding --
+
+ '-- EndPadding': {
+ 'type': 'separator',
+ 'title': ' ',
+ },
+}
+
+export default meta
diff --git a/apps/docs/pages/app-config.mdx b/apps/docs/pages/app-config.mdx
new file mode 100644
index 0000000..89ac272
--- /dev/null
+++ b/apps/docs/pages/app-config.mdx
@@ -0,0 +1,245 @@
+import { FileTree, Steps } from 'nextra/components'
+import { Image } from '@app/primitives'
+
+
+
+# Configuring your Universal App
+
+```ts copy
+import { appConfig } from '@app/config'
+```
+
+
+
+
+
+
+
+
+
+This doc will cover the basic of configuring your app:
+
+- [App Context Flags](#app-context-flags)
+- [Environment Variables](#managing-environment-variables)
+- [Managing Dependencies](#managing-dependencies)
+- [Mobile App specific config](#managing-expo-config)
+- [Web / next.js specific config](#managing-nextjs-config)
+
+## App Context Flags
+
+A good reason to use `@app/config` is for context flags.
+
+Your universal app can run in different environments like iOS, a web browser, Android, the server, etc.
+
+Sometimes you want to debug, test or show something only in a specific environment.
+
+For example:
+
+```ts
+import { appConfig } from '@app/config'
+
+// Platform flags
+if (appConfig.isWeb) console.log('Running in the browser')
+if (appConfig.isMobile) console.log('Running on a mobile device')
+
+// Runtime flags
+if (appConfig.isServer) console.log('Running on the server')
+
+// Framework flags
+if (appConfig.isExpo) console.log('Running app with Expo')
+if (appConfig.isNext) console.log('Running app with Next.js')
+
+// Debug flags
+if (appConfig.isExpoWebLocal) console.log('Running locally on web with Expo')
+if (appConfig.isNextWebLocal) console.log('Running locally on web with Next.js')
+```
+
+## Managing environment variables
+
+### Local env vars
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Local environment variables can be managed in each app's `.env.local` file.
+
+The `.env.example` files can be copied into `.env.local` using the following command:
+
+```shell
+npm run env:local
+```
+
+- Use `NEXT_PUBLIC_` to expose your environment variable in your next.js front-end
+- Use `EXPO_PUBLIC_` to expose your environment variable in your expo front-end
+
+We suggest that for each environment variable you add in these `.env.local` files, you also add an entry in `appConfig.ts`. For example:
+
+```ts {6, 7} filename="appConfig.ts"
+export const appConfig = {
+ // ...
+
+ // - Secrets -
+
+ // Don't use NEXT_PUBLIC_ / EXPO_PUBLIC_ prefixes here to make sure these are undefined client-side
+ appSecret: process.env.APP_SECRET,
+
+ // ...
+}
+```
+
+### Env vars in Next.js
+
+For development, staging & production environments, check the next.js docs:
+https://nextjs.org/docs/app/building-your-application/configuring/environment-variables
+
+> Note that you should treat environment variables as if they could be inlined in your bundle during builds & deployments
+> This means dynamically retrieving environment variables from e.g. `process.env[someKey]` might not work
+> It also means that you should never prefix with `NEXT_PUBLIC_` for sensitive / private keys
+
+#### Managing secrets
+
+App secrets that should only be available server-side should:
+- not be prefixed with `NEXT_PUBLIC_` (this omits then from the client-side)
+- not be included in Expo's `.env.local` file (all expo env vars are public and included in bundle by default)
+
+### Env vars in Expo
+
+For development, staging & production environments, check the expo docs:
+https://docs.expo.dev/guides/environment-variables/
+
+> Note that Expo will inline environment variables in your bundle during builds & deployments
+> This means dynamically retrieving environment variables from e.g. `process.env[someKey]` will not work
+> It also means that you should never include sensitive / private keys
+
+### Defining endpoints
+
+Your most important environment variables are likely your API endpoints:
+
+```shell filename=".env.local"
+## --- General --------------------------------------------------------------------------------- */
+## -i- Env vars that should always be present & the same locally, independent of the simulated environment
+## --------------------------------------------------------------------------------------------- */
+
+NEXT_PUBLIC_BASE_URL=http://localhost:3000
+NEXT_PUBLIC_BACKEND_URL=http://localhost:3000
+NEXT_PUBLIC_API_URL=http://localhost:3000/api
+NEXT_PUBLIC_GRAPH_URL=http://localhost:3000/api/graphql
+```
+
+> Which you can then consolidate in your `appConfig.ts`
+
+```ts filename="appConfig.ts"
+export const appConfig = {
+ // ...
+
+ // - Server URLs -
+
+ baseURL: process.env.NEXT_PUBLIC_BASE_URL || process.env.EXPO_PUBLIC_BASE_URL,
+ backendURL: process.env.NEXT_PUBLIC_BACKEND_URL || process.env.EXPO_PUBLIC_BACKEND_URL,
+ apiURL: process.env.NEXT_PUBLIC_API_URL || process.env.EXPO_PUBLIC_API_URL,
+ graphURL: process.env.NEXT_PUBLIC_GRAPH_URL || process.env.EXPO_PUBLIC_GRAPH_URL,
+
+ // ...
+}
+```
+
+> For the examples above, we've already set these up for local use.
+
+## Feature and Domain config
+
+
+
+
+
+
+
+
+
+
+
+
+As stated in our [project structure](/project-structure) docs, workspaces should define their own `package.json` file.
+
+That means that each feature or package workspace can have its own dependencies.
+
+> This helps keep things modular, portable and colocated so features remain copy-pasteable.
+
+### Expo SDK compatible packages
+
+For your Expo mobile app, it's important to install packages that are compatible with your current Expo SDK.
+
+However, using Expo's own `expo install` command will only install packages in the expo app workspace, which we don't want.
+
+To help with this, we've created a script that will install SDK compatible packages in the correct workspace:
+
+```shell copy
+npm run add:dependencies
+```
+
+This will ask you where you'd like to install the package:
+
+```shell {5}
+>>> Modify "your-project-name" using custom generators
+
+? Which workspace should we install the Expo SDK compatible versions in? (Use arrow keys)
+ @db/driver
+❯ @green-stack/core
+ @app/registries
+ @app/core
+```
+
+You can then specify one or multiple packages to have their SDK compatible versions installed.
+
+## Managing Expo config
+
+Check the Expo docs for configuring your mobile app with Expo:
+
+- https://docs.expo.dev/workflow/configuration/
+- https://docs.expo.dev/eas/json/
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Managing Next.js config
+
+Check the Next.js docs for configuring your web app with Next.js:
+
+- https://nextjs.org/docs/app/building-your-application/configuring
+- https://nextjs.org/docs/app/api-reference/next-config-js
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/docs/pages/core-concepts.mdx b/apps/docs/pages/core-concepts.mdx
new file mode 100644
index 0000000..0c23c67
--- /dev/null
+++ b/apps/docs/pages/core-concepts.mdx
@@ -0,0 +1,512 @@
+import { FileTree } from 'nextra/components'
+import { View, Image } from '@app/primitives'
+import { ImageZoom } from 'nextra/components'
+import { Hidden } from '../components/Hidden'
+
+
+
+# Core concepts
+
+Unlike most boilerplates, this starterkit is not just a collection of tools and libraries. It's a way of working that's been designed to be both scalable and portable from the start.
+
+This document will cover the **core concepts that make this starterkit unique**:
+
+- **[Universal, from the start](/core-concepts#universal-from-the-start)**
+- **[Evergreen with the GREEN stack](/core-concepts#the-green-stack)**
+- **[Single Sources of Truth](/core-concepts#single-sources-of-truth)**
+- **[Design for copy-paste](/core-concepts#design-features-for-copy-paste)**
+
+
+
+## Universal, from the start
+
+It's a lot harder to add a mobile app later, than to build for web and app stores from the start.
+
+Luckily, in Next.js web-apps, as well as iOS and Android apps with Expo, using `react-native` for the UI layer will keep your code mostly write-once.
+
+No extra time wasted writing features twice or more, as 90% of react-native styles translate well to web. All it takes is to start with react-native's `View` / `Text` / `Image` primitives. They'll get transformed to HTML dom nodes on web.
+
+### Build for nuanced user preferences
+
+Think about your users for a second. Now think about your own behaviour when interacting with software.
+
+You likely prefer your phone for some apps. But when you're at your desk in an office, it comes in handy to just type a url and continue from there.
+
+Depending on the context, your potential users will also have a preference for web or mobile. That no longer matters when you build for all devices and platforms at no extra cost. Everyone is supported.
+
+### Capture on web, convert on mobile
+
+For startups and ambitious founders, building a Universal App can be a huge **competitive advantage**.
+
+When you do market research and see comments on a competitor's social media asking for an Android / iOS / Web version, you can swoop right in. Meanwhile the competition likely still has to hire an entire other team to rebuild the app for whatever platform they're missing. Being available on each platform also **inspires trust**, and trust is a major key to all sales.
+
+With SEO you could even **show up where users are already searching** for a solution. It's essentially **free organic traffic**. Customer acquisition costs when it comes to paid ads is also cheaper for web than mobile ads.
+
+Why not show a **quick interactive demo** of the app right on your landing page? The urge to finish what's been started might kick in, making it more likely they sign up web-first, and join those who prefer mobile later in finding and installing the app from the App Store.
+
+On mobile, your **app icon** is now listed on their phone, taking up valuable screen real estate. A **daily reminder your brand exists**. Mobile also provides stronger notification systems to retarget users and keep them engaged. This is why, in the e-commerce space, *mobile often drives more sales and conversions* than web does.
+
+### Universal (Deep)links
+
+Our shared digital experience thrives on links. Expo-router is the *first ever url based routing system for mobile*. Deeplinking, where you set up your mobile app to **open the right screen for specific URLs**, happens automatically in Expo-router. When people share anything from the app or the web version, the links will just work. Whether they've installed the app or not.
+
+Once users learn they can just share links, it turns existing users into ambassadors.
+
+### More with less
+
+Even if you're just freelancing or working as a digital product studio, you'll be able to deliver the same app on multiple platforms. This can give you the advantage you need to win more clients. Or you could charge a premium for the same amount of work.
+
+> "Web vs. Native is dead. Web **and** Native is the future."
+> - Evan Bacon, expo-router maintainer
+
+
+
+## The GREEN stack
+
+### Take what works, make it better
+
+The best way to get really good and really fast at what you do, is to keep using the same tools for a longer period of time. That means not reinventing the wheel. For every new project. Again. And again. You want your way of working to be evergreen.
+
+### Expo + Next.js for best DX / UX
+
+When building Universal Apps, the stack you choose should optimize for each device and platform you're trying to target. This is the main reason why this starterkit / tech stack / way of working focuses entirely on Next.js and Expo.
+
+These two meta frameworks are simply best in class when it comes to DX and UX optimizations for their target platforms. NextJS does all it can to set you up for success when it comes to essential SEO things like web-vitals.
+
+Expo has made starting, building, testing, deploying, submitting and updating react-native apps just as easy. Apps made with Expo also result in actual native apps, which have the performance and responsiveness that comes with using platform primitives, because that's what it renders under the hood.
+
+### React-Native for write-once UI
+
+The dream of React has always been "write-once, use anywhere". For now, react-native and react-native-web gets us 90% of the way there. In the future, things like react-strict-dom will likely bump that number up higher. Until then, pairing react-native with Nativewind or a full-blown universal styling system like Tamagui seems like the way to go.
+
+### Universal Data Fetching with GraphQL
+
+GraphQL's ability to **query data from the server, browser and mobile** puts it in the unique position to service all three platforms. Paired with `react-query` for optimizing the caching of queries, and [gql.tada 🪄](https://gql-tada.0no.co/get-started/#a-demo-in-128-seconds) for auto inferring types from the schema + query definitions, integrated with a universal router, and you have a great universal initial data fetching solution. Type-safe end to end.
+
+*This doesn't mean you only have GraphQL at your disposal.* Resolvers in the GREEN stack are quite flexible. Designed so that porting them to `tRPC` (through a plugin) and/or Next.js API routes is quick and easy to do. From experience, we're convinded GraphQL works best when used in an RPC manner. Instead of a REST-like graph you can explore each domain of your data with, we instead urge you to create resolvers in function of the UI they need data for.
+
+> To illustrate RPC-style queries in your mind, think of `getDashboardData()` vs. having to call 3 separate `Orders()`, `Products()`, `Transactions()` type resolvers to achieve the same thing.
+
+When used in this manner, quite similar to tRPC, it remains the best for initial data-fetching. Though mutating data might be better served as a tRPC call or API route POST / PUT / DELETE request.
+
+To avoid footguns, the starterkit provides a way of working that can automate a bunch of the hard stuff when it comes to doing GraphQL, or data resolvers in general, the right way.
+
+**This includes things like:**
+- auto-generating the entire **schema from Zod definitions**
+- auto-generating fetcher functions and **`react-query` hooks** from Zod definitions
+- keeping schema definitions a 1 on 1 match with your RPC / "command-like" resolver functions
+- universal `graphqlRequest()` util to **auto infer types** from Zod input & output schemas
+- ... or using [gql.tada](https://gql-tada.0no.co/get-started/#a-demo-in-128-seconds) for type hints / infers when only requesting specific fields.
+
+### Tech that's here to stay.
+
+To bring it all together, you could say the GREEN stack stands for:
+- ✅ GraphQL
+- ✅ React-Native
+- ✅ Expo
+- ✅ Next.js.
+
+The second "E" is in there because Expo, with it's drive to bring react-native to web, is doing double the lifting.
+
+> ...but in reality, the main goal of this stack is simply to stay 'Evergreen'
+
+These core technologies and the ecosystems around them create a stack that's **full-featured yet flexible** where needed. Other included essentials like **Typescript, Tailwind and Zod**, have gained enough popularity, adoption, frequent funding and community support they're just as likely to be around for a long time.
+
+It's a stack you can stick, evolve and perfect your craft with for a longer period of time.
+
+
+
+## Single Sources of Truth
+
+Think of all the places you may need to (re)define the shape of data.
+
+- ✅ Types
+- ✅ Validation
+- ✅ DB models
+- ✅ API inputs & outputs
+- ✅ Form state
+- ✅ Documentation
+- ✅ Mock & test data
+- ✅ GraphQL schema defs
+
+Quite an extensive list for what is **essentially describing the same data**.
+
+Ideally, you could define the shape of your data just once, and have it be transformed to these other formats where necessary:
+
+### A strong toolkit around Zod schemas
+
+Schema validation libraries like zod are actually uniquely positioned to serve as the base of transforming to other formats. Zod even has the design goal to be as compatible with Typescript as possible. There's almost nothing you can define in TS that you can't with Zod. Which is why you can infer super accurate types from your Zod schemas.
+
+With the hardest 2 of 8 data definition scenario's tackled, the starterkit comes with utils that help convert Zod schemas to the others mentioned above:
+
+- [createSchemaModel()](TODO) - Create a DB model from Zod schema
+- [createDataBridge()](/data-resolvers#start-from-a-databridge) - Combines zod input & output schemas for type-safe API bridges
+- [createResolver()](/data-resolvers#add-your-business-logic) - Bind a zod bridge to a promise into a portable data resolver
+- [createNextRouteHandler()](/data-resolvers#turn-resolver-into-an-api-route-handler) - Transform your zod-powered resolver into a Next.js API route
+- [createGraphResolver()](/data-resolvers#graphql-without-the-hassle) - Transform your zod-powered resolver into a GraphQL Mutation or Query
+- [createGraphSchemaDefs()](TODO) - Auto generate the GraphQL SDL statements for your schema.graphql
+- [bridgedFetcher()](/data-fetching#build-a-fetcher-function) - Transform a zod bridge into a typed GraphQL fetcher
+- [createQueryBridge()](/data-fetching#define-data-fetching-steps-in-screens) - Use a typed (GraphQL) fetcher to query initial daya for universal routes
+- [getDocumentationProps()](TODO) - Combine zod prop defs with a React component for autogenerated docs
+
+### Zod for Automatic docs
+
+Writing documentation is essential, *but often requires time teams feel they don't have.*
+
+Any successfull project will eventually need documentation though. Especially if you want others to build on top of your work:
+
+> "**Documentation Drives Adoption**" - Storybook
+
+Once it's time to scale up the team, you'll definitely want them. Ideally, you can onboard new devs rather quickly so they can add value sooner. Good docs reduce how much mentoring new people need from your senior developers.
+
+*However*, as a new or scaling startup, both docs and onboarding are not necessarily the thing you want to "lose" time on. Which is why, at least at the start:
+
+> "**Sometimes, the best docs are the ones you don't have to write yourself.**"
+> -- Founders that value their time
+
+This is where `MDX` and **Zod schemas as single sources of truth** are a great match. Using the Starterkit's `with/automatic-docgen` plugin, your components and API's will document themselves.
+
+How? By reading the example & default values of a component's Zod prop schema and generating an `MDX` file from it. The file will then render the component and provide a table with prop names, descriptions, nullability and interactive controls you can preview the different props with.
+
+Check out a live example for the [Button](/@app-core/components/Button) component in action:
+
+[](/@app-core/components/Button)
+
+> You can think of it like a component Storybook where you could build components in isolation on the docs page. Except these docs automatically grow with your project as a result of your way of working.
+
+More about this pattern in the [Single Sources of Truth](/single-sources-of-truth) and [Automations](TODO) docs.
+
+
+
+## Design features for copy-paste
+
+What tools like Tailwind and Shad-CN enabled for copy-pasting components, we aim to replicate for entire features and domains.
+
+That includes the UI, hooks, logic, resolvers, API's, zod schemas, db models, fetchers, utils and more.
+
+### Portable Workspace Folder Structure
+
+While individual utils, components and styles can be quite easy to reuse across projects these days, entire features are harder to port from one project to another. It's because most project structures don't lean themselves to copy-pasting a single folder between projects in order to reuse a feature.
+
+*This often stems from grouping on the wrong level, **such as a front-end vs. back-end split.***
+
+It does become possible once you start grouping code together on the feature or domain level, as a reusable workspace:
+
+```shell
+features/@some-feature
+
+ └── /schemas/... # <- Single sources of truth
+ └── /models/... # <- Reuses schemas
+ └── /resolvers/... # <- Reuses models & schemas
+
+ └── /components/...
+ └── /screens/... # <- Reuses components
+ └── /routes/... # <- Reuses screens
+ └── /api/... # <- Reuses resolvers
+
+ └── /assets/...
+ └── /icons/... # <- e.g. svg components
+ └── /constants/...
+ └── /utils/...
+```
+
+Each folder that follows this structure should have its own `package.json` file to define the package name and dependencies. This way, you can easily copy-paste a feature or domain from one project to another, and NPM will have it work out of the box.
+
+Here's what this might look like in the full project.
+Don't hesitate to open the `/apps/`, `/features/` or `/packages/` folders:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+### Git based plugins you can learn from - (⏳ Coming Soon)
+
+> **"The best way to learn a new codebase is in the Pull Requests."** - Theo Browne, @t3dotgg
+
+
+
+The best plugin system is one that's close to a workflow you're already used to.
+
+Github PR's and git branches are typically better for a number of reasons:
+
+- ✅ Inspectable diff you can review with a team
+- ✅ Able to check out and test first
+- ✅ Optionally, add your own edits before merging
+
+Finally, PR based plugins solve common issues with other templates:
+
+- ✅ Pick and choose your own preferred tech stack
+
+### Workspace Drivers - Pick your own DB / Auth / Mail / ...
+
+Drivers are a way to abstract away the specific implementation of e.g. your specific database, storage or authentication system. This allows you to switch between different implementations without changing the rest of your code.
+
+**Typically, drivers are class based**, *and often back-end only*. However, we've found a different way of providing a familiar interface, while still providing a familiar way of working that works across front and back-end lines.
+
+The key is **defining drivers as workspace packages**:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```typescript {1}
+// Instead of using specific imports from a package...
+import { signIn } from '@clerk/clerk-expo'
+```
+
+`⬇⬇⬇`
+
+```typescript {1, 4} /@auth/ /driver/2
+// You can (optionally) import and use a familiar API from a driver workspace:
+import { signIn } from '@auth/driver'
+
+// OR, when mixing solutions, import from multiple drivers:
+import { users } from '@db/mongoose' // <- e.g. User data saved in mongo
+import { settings } from '@db/airtable' // <- e.g. Configuration saved in Airtable
+```
+
+You can have multiple drivers merged, *or "installed"*, but you should pick the main one in `appConfig.ts`:
+
+```ts {3} filename="features / @app-core / appConfig.ts"
+export const appConfig = {
+
+ // Here's how you'd set the main driver to be used:
+ drivers: createDriverConfig({
+ db: DRIVER_OPTIONS.db.mockDB, // -> use as '@db/driver'
+ auth: DRIVER_OPTIONS.auth.clerk // -> @auth/driver
+ mail: DRIVER_OPTIONS.email.resend // -> @mail/driver
+ }),
+
+} as const
+```
+
+Combining multiple drivers for the same solution can be a good idea if e.g.:
+- Specific types of data lend themselves better to a different provider
+- You're migrating from one provider to another
+
+However, if you do, you should always pick a main one to make sure e.g. `@db/driver` corresponds to the main solution used.
+
+> **Drivers are fully optional, just like most of the suggested ways of working.**
+> You won't experience any issues if you don't use them.
+
+> However, you might find it more difficult to keep features / domains / workspaces easily copy-pasteable without using these kinds of abstractions.
+
+Drivers and plugins within the starterkit's Way of Working have been designed to:
+- Use `Zod` for validating **familiar API across implementations/options**
+- Use `Zod` for providing both **types and schemas for said plugin**
+- Help keep features **as copy-pasteable as possible**
+
+
+
+## Maximizing time saved
+
+> The big difference between a weekend boilerplate and a value adding starterkit is how far it goes to save you time.
+
+**Boilerplates:**
+- typically only provide a better starting point.
+- might save you weeks of setup, sure, but afterwards they rarely do much for you.
+- not always aimed at experienced developers (types and scalable architecture)
+- might end up still changing a bunch / wasting time
+
+There are **more ways to save time as a developer** though. To recap our core concepts:
+
+- ✅ *Starting universally*, building mostly write-once for each platform saves time later on and retains your ability to do **fast iterations while also serving way more users**.
+- ✅ Keeping features **copy-pasteable and portable between projects**.
+- ✅ *Recommended way of working* that also saves time. By encouraging you to define data shapes once, transform them to other formats where necessary, and providing a toolkit around them.
+- ✅ Generators to quickly *scaffold out new schemas, models, resolvers, forms, fetchers, components, hooks, screens, routes from CLI.*
+
+### Generators to skip boilerplate
+
+This zod-based way of working, combined with the predictability of file-system based routing, can lead to some huge time saved when you automate the repetitive parts.
+
+The starterkit comes with a number of generators that can help you skip the repetitive boilerplate code and manual linking of files and objects when creating new features:
+
+- `npx turbo gen add-dependencies` - Add Expo SDK compatible dependencies to a workspaces
+- `npx turbo gen workspace` - Add a new feature or package workspace folder to the project
+- `npx turbo gen schema` - Add a new Zod schema to serve as single source of truth
+- `npx turbo gen model` - Add a new DB model to the project based on a Zod schema
+- `npx turbo gen resolver` - GraphQL resolver *and* API route based on Zod input and output
+- `npx turbo gen form` - Create form hooks for a specific schema in your workspaces
+- `npx turbo gen route` - New universal route + screen, and integrate with a resolver
+- `npx turbo gen domain` - Like `gen workspace` on steroids, full domain with all the above
+
+### AI -- Codegen +
+
+
+
+Using generators can be a safe way to quickly set-up the boilerplate and linking, and you could have AI fill in the blanks of the business logic.
+
+Though, ofcourse, you will need to understand and double check whatever the AI generates for you. If you do, combining old-school generators with tools like Cursor or GitHub Copilot can be a great way to further maximize speed.
+
+FullProduct.dev will soon add prompts and rulesets to help you build out features with our way of working.
+
+### Start scalable, without the effort
+
+With these core-concepts combined, we believe we can provide Typescript and React devs with a really powerful way of working that is at all times:
+
+- ✅ Opinionated yet flexible
+- ✅ Built for maximum code reuse
+- ✅ Universal, write-once, reach any device
+- ✅ Helping you easily onboard and scale up the team
+- ✅ A huge time-saver at both the start and during the project
+
+All without having to spend the time figuring it all out yourself.
+
+If you're ready to dive deeper into these topics, check out the rest of the docs.
+
+
+
+
diff --git a/apps/docs/pages/data-fetching.mdx b/apps/docs/pages/data-fetching.mdx
new file mode 100644
index 0000000..113535b
--- /dev/null
+++ b/apps/docs/pages/data-fetching.mdx
@@ -0,0 +1,821 @@
+import { FileTree, Steps } from 'nextra/components'
+import { Image } from '@app/primitives'
+
+
+
+# Universal Data Fetching
+
+**This doc covers our recommended way to set up data-fetching in a way that:**
+- ✅ Remains portable across different projects
+- ✅ Reuses as much code between the front and back-end as possible
+- ✅ Building a single source of truth for your APIs and using them in the front-end
+- ✅ Uses `react-query` to execute the actual data-fetching logic on the server, browser, iOS and Android
+
+We suggest defining your data-fetching logic in the same place as the screen component requiring that data:
+
+```shell {4, 6}
+features/@some-feature
+
+ └── /resolvers/... # <- Defines portable data bridges
+ └── /screens/... # <- Reuses data bridges on front-end
+ └── PostDetailScreen.tsx
+ └── /routes/... # <- Reuses screens and bridges for data-fetching
+ └── posts /[slug]/ index.tsx # <- e.g. reuse 'PostDetailScreen.tsx'
+```
+
+This workspace structure will help keep your features as copy-pasteable and portable across different projects by avoiding a front-end vs. back-end split.
+
+For an example of what this might look like in an actual project, check the example below. Don't hesitate to click open some of the folders to get a better idea of how portable routes with data-fetching are structured:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+> The recommended and easiest way to create a new route in a workspace is to use the Route Generator:
+
+
+
+## Using the Route Generator
+
+```shell
+npm run add:route
+```
+
+> The turborepo route generator will ask you some questions, like which resolver you'd like to fetch data from, and will generate the empty screens and routes folders in the workspace of your choico with data fetching already set up:
+
+```shell {6, 9}
+>>> Modify "your-project-name" using custom generators
+
+? Where would you like to add this new route? # -> features/@app-core
+? What should the screen component be called? # -> NewRouteScreen
+? What url do you want this route on? # -> "/examples/[slug]"
+? Would you like to fetch initial data for this route from a resolver?
+
+ getPost() resolver -- from: '@app/some-feature'
+❯ getExample() resolver -- from: '@app/other-feature'
+ getData() -- from: '@app/other-feature'
+ No, this screen doesn't need to fetch data to work
+ No, I'll figure out data-fetching myself # (editable dummy example)
+```
+
+> The list of available resolvers is based on the data bridges available in each workspace's `/resolvers/` folder.
+
+Pick the last option if you want to create a new bridge and resolver alongside the route and screen.
+
+In which case the generated boilerplate might look like this:
+
+```shell {4, 5} /(if chosen)/
+>>> Changes made:
+ • /features/@app-core/resolvers/newResolver.bridge.ts # ← New bridge (if chosen)
+ • /features/@app-core/resolvers/newResolver.query.ts # ← New graphql fetcher (if chosen)
+ • /features/@app-core/screens/NewRouteScreen.tsx # ← New screen with fetch config
+ • /features/@app-core/routes/examples/[slug]/index.tsx # (add)
+
+>>> Success!
+```
+
+To explain what the generator does for you, have a look at the recommended steps to add data fetching manually:
+
+
+
+## Manually set up Data Fetching
+
+
+
+There are 3 environments to consider when providing dynamic data to your screens:
+
+- **Server-side rendering (SSR)** using the **[executable schema](https://the-guild.dev/graphql/tools/docs/generate-schema)**
+- **Client-side rendering (CSR)** in the browser (**hydration** or fetch)
+- **Mobile App** client in Expo (fetch only)
+
+To fetch data the same way in all three, we've written two helpers:
+- `createQueryBridge()` - Build **instructions for data-fetching with `react-query`** (step 3)
+- `` - Component that **uses the bridge to fetch data** in each environment (step 4)
+
+This is not the only way to do data-fetching, but our recommended way to do so:
+
+
+
+### Start with a Data Bridge
+
+> If you already set up a [DataBridge](/data-resolvers#start-from-a-databridge) while [building your resolver](/data-resolvers), you can skip to the next step.
+
+
+
+
+
+
+
+
+
+
+
+
+
+What is a Data Bridge?
+
+---
+
+Schemas serve as the single source of truth for your data shape. But what about API shape?
+
+You can combine input and output schemas into a `bridge` file to serve as the single source of truth for your API resolver. You can use `createDataBridge` to ensure all the required fields are present:
+
+```ts {14, 17} /createDataBridge/1,3 filename="getPost.bridge.ts"
+import { Post } from '../schemas/Post.schema'
+import { createDataBridge } from '@green-stack/schemas/createDataBridge'
+import { schema } from '@green-stack/schemas'
+
+/* -- Schemas ------------ */
+
+// You can create, reuse or edit schemas here, e.g.
+export const GetPostArgs = schema('GetPostArgs', {
+ slug: z.string(),
+})
+
+/* -- Bridge ------------- */
+
+export const getPostBridge = createDataBridge({
+ // Assign schemas
+ inputSchema: GetPostArgs,
+ outputSchema: Post,
+
+ // GraphQL config
+ resolverName: 'getPost',
+ resolverArgsName: 'PostInput',
+
+ // API route config
+ apiPath: '/api/posts/[slug]',
+ allowedMethods: ['GET', 'GRAPHQL'],
+})
+```
+
+Think of a **"Databridge"** as a literal *bridge between the front and back-end*.
+
+It's a metadata object you can use from either side to provide / transform into:
+
+- ✅ Input and output **types + validation + defaults**
+- ✅ GraphQL **schema definitions** in `schema.graphql`
+- ✅ The query string to call our GraphQL API with
+
+*There's two reasons we suggest you define this "DataBridge" in a separate file*:
+
+- 1. **Reusability**: If kept separate from business logic, you can reuse it in both front and back-end code.
+- 2. **Consistency**: Predicatable patterns make it easier to build automations and generators around them.
+
+> For this reason, we suggest you add `.bridge.ts` to your bridge filenames.
+
+
+
+### Build a fetcher function
+
+> We suggest you use `GraphQL` for building your data-fetching function.
+
+*It's not the only way you could do it*, but our Bridge + GraphQL fetcher helpers will ensure your query:
+
+- ✅ can **execute on the server, browser, iOS and Android** (better for universal routes)
+- ✅ can **automatically build the query string** from input and output **schemas in the bridge**
+- ✅ is **auto typed** *when using the bridge to provide the query automagically*
+- ✅ **supports custom queries** that are also auto typed, with `gql.tada` (as an alternative)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+The easiest way to create a fetcher is to use the `bridgedFetcher()` helper:
+
+```ts {3} /bridgedFetcher/3 filename="getPost.query.ts"
+import { getPostBridge } from './getPost.bridge'
+// ☝️ Reuse your data bridge
+import { bridgedFetcher } from '@green-stack/schemas/bridgedFetcher'
+// ☝️ Universal graphql fetcher that can be used in any JS environment
+
+/* --- getPostFetcher() --------- */
+
+export const getPostFetcher = bridgedFetcher(getPostBridge)
+```
+
+This will automatically build the query string with all relevant fields from the bridge.
+
+
+How to use custom queries?
+
+---
+
+To write a custom query with only certain fields, you can use our `graphql()` helper *with* `bridgedFetcher()`:
+
+```ts {3, 11, 21, 31, 41} filename="getPost.query.ts"
+import { ResultOf, VariablesOf } from 'gql.tada'
+// ☝️ Type helpers that interface with the GraphQL schema
+import { graphql } from '../graphql/graphql'
+// ☝️ Custom gql.tada query builder that integrates with our types
+import { bridgedFetcher } from '@green-stack/schemas/bridgedFetcher'
+// ☝️ Universal graphql fetcher that can be used in any JS environment
+
+/* --- Query ----------------------- */
+
+// VSCode and gql.tada will help suggest or autocomplete thanks to our schema definitions
+export const getPostQuery = graphql(`
+ query getPost ($getPostArgs: GetPostInput) {
+ getPost(args: $getPostArgs) {
+ slug
+ title
+ body
+ }
+ }
+`)
+
+// ⬇⬇⬇ automatically typed as ⬇⬇⬇
+
+// TadaDocumentNode<{
+// getPost(args: { slug: string }): {
+// slug: string | null;
+// title: boolean | null;
+// body: boolean | null;
+// };
+// }>
+
+// ⬇⬇⬇ can be turned into reusable types ⬇⬇⬇
+
+/* --- Types ----------------------- */
+
+export type GetPostQueryInput = VariablesOf
+
+export type GetPostQueryOutput = ResultOf
+
+/* --- getPostFetcher() --------- */
+
+export const getPostFetcher = bridgedFetcher({
+ ...getPostBridge, // <- Reuse your data bridge ...
+ graphqlQuery: getPostQuery, // <- ... BUT, use our custom query
+})
+```
+
+
+
+> Whether you use a custom query or not, you now have a GraphQL powered fetcher that:
+
+- ✅ Uses the executable graphql schema serverside
+- ✅ Can be used in the browser or mobile using fetch
+
+### Define data-fetching steps in `/screens/`
+
+> You'll be using your fetcher in `createQueryBridge()` in one of our universal screen components:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+What `createQueryBridge()` does is provide a set of instructions to:
+
+- ✅ Transform url params into query variables for our fetcher function
+- ✅ Fetch data using our custom graphql fetcher and `react-query`
+- ✅ Map the fetched data to props for our screen Component
+
+> We define these steps here so we can reuse this query bridge to extract types for our screen props:
+
+```tsx {2, 7, 24, 30} /createQueryBridge/ /routeDataFetcher/ /postData/1,3 filename="PostDetailScreen.tsx"
+import { createQueryBridge } from '@green-stack/navigation'
+import { getPostFetcher } from '@app/some-feature/getPost.query'
+import type { HydratedRouteProps } from '@green-stack/navigation'
+
+/* --- Data Fetching --------------- */
+
+export const queryBridge = createQueryBridge({
+
+ // 1. Transform the route params into things useable by react-query (e.g. 'slug')
+ routeParamsToQueryKey: (routeParams) => ['getPost', routeParams.slug],
+ routeParamsToQueryInput: (routeParams) => ({ getPostArgs: { slug: routeParams.slug } }),
+
+ // 2. Provide the fetcher function to be used by react-query
+ routeDataFetcher: getPostFetcher,
+
+ // 3. Transform fetcher output to props after react-query was called
+ fetcherDataToProps: (fetcherData) => ({ postData: fetcherData?.getPost }),
+})
+
+// ⬇⬇⬇ Extract types ⬇⬇⬇
+
+/* --- Types ----------------------- */
+
+type PostDetailScreenProps = HydratedRouteProps
+
+// ⬇⬇⬇ Use fetcher data in screen component ⬇⬇⬇
+
+/* --- --------------- */
+
+const PostDetailScreen = (props: PostDetailScreenProps) => {
+
+ // Query results from 'fetcherDataToProps()' will be added to it
+ const { postData } = props
+ // ☝️ Typed as {
+ // postData: {
+ // slug: string,
+ // title: string,
+ // body: string,
+ // }
+ // }
+
+ // -- Render --
+
+ return (...)
+}
+```
+
+### Use the bridge and screen in `/routes/`
+
+> We now have the individual parts of our data-fetching, all that's left is to bring it all together in a route:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+This is where `UniversalRouteScreen` comes in to **execute each step** op the `queryBridge` **in sequence** until we get to the final props to be provided to the screen.
+
+```tsx {2, 7} /queryBridge/3 /PostDetailScreen/2 filename="features / @some-feature / routes / index.tsx"
+import { PostDetailScreen, queryBridge } from '@app/screens/PostDetailScreen'
+import { UniversalRouteScreen } from '@app/navigation'
+
+/* --- /posts/[slug] ----------- */
+
+export default (props) => (
+
+)
+```
+
+In the same `/routes/index.tsx` file, you can add the [Next.js routing config](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config)
+
+```tsx {} filename="features / @app-core / routes / index.tsx"
+// -i- Export any other next.js routing config here
+export const dynamic = 'auto'
+export const dynamicParams = true
+export const revalidate = false
+export const fetchCache = 'auto'
+export const runtime = 'nodejs'
+export const preferredRegion = 'auto'
+export const maxDuration = 5
+```
+
+> 💡 Check Next.js [route segment config](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config) later to understand the options you can set here.
+
+We'll be re-exporting this route segment config in the next step. We'll keep it in the same file as the main route component for colocation and enabling `@green-stack/scripts` to automatically re-export it.
+
+### Re-export the route in your apps
+
+```shell
+npm run link:routes
+```
+
+This will re-export the route from your workspace to your Expo and Next.js apps:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+> You could do this manually as well, but... why would you?
+
+
+
+This concludes our recommended flow for manually setting up data-fetching for routes.
+
+> **However, you might want to fetch data at the component level**, or even mutate / update data instead. For that, we recommend using `react-query` directly in your components:
+
+
+
+## Using `react-query`
+
+[react-query](https://tanstack.com/query/latest) is a library that helps you manage your data-fetching logic in a way that's easy to use and understand. It provides tools to fetch, cache, and update data in an efficient and flexible way.
+
+It's built around the concept of **queries** and **mutations**:
+
+- **Queries** are used to fetch data from an API
+- **Mutations** are used to send data to an API
+
+> Queries are the most common use case, and the one we've been focusing on here.
+
+
+
+### `UniversalRouteScreen`
+
+For example, the **UniversalRouteScreen wrapper** *uses react-query to fetch data* for a route:
+- **serverside**, during server side rendering (SSR)
+- **in the browser**, during client side rendering (CSR)
+- **in the mobile app**, fetching from the client
+
+
+How do our universal routes fetch initial data with react-query?
+
+---
+
+Here's a quick look at how `UniversalRouteScreen` uses `react-query` to fetch data:
+
+```tsx {13, 25, 49} filename="UniversalRouteScreen.tsx"
+// Props
+const { queryBridge, routeScreen: RouteScreen, ...screenProps } = props
+
+// Extract the fetching steps from our queryBridge
+const { routeParamsToQueryKey, routeParamsToQueryInput, routeDataFetcher } = queryBridge
+const fetcherDataToProps = queryBridge.fetcherDataToProps
+
+// Combine all possible route params to serve as query input
+const { params: routeParams, searchParams } = props
+const nextRouterParams = useRouteParams(props)
+const queryParams = { ...routeParams, ...searchParams, ...nextRouterParams }
+
+// -- Query --
+
+const queryClient = useQueryClient()
+const queryKey = routeParamsToQueryKey(queryParams) // -> e.g. ['getPost', 'some-slug']
+const queryInput = routeParamsToQueryInput(queryParams) // -> e.g. { getPostArgs: { slug: 'some-slug' } }
+
+const queryConfig = {
+ queryKey,
+ queryFn: async () => routeDataFetcher(queryInput), // <- Uses our fetcher
+ initialData: queryBridge.initialData, // <- Optional initial data
+}
+
+// -- Browser & Mobile --
+
+if (isBrowser || isMobile) {
+
+ // Execute the query as a hook
+ const { data: fetcherData } = useQuery({
+ ...queryConfig, // <- See above, includes our 'queryFn' / fetcher
+ initialData: {
+ ...queryConfig.initialData,
+ ...hydrationData, // <- Only on browser (empty on mobile)
+ },
+ refetchOnMount: shouldRefetchOnMount,
+ })
+
+ const routeDataProps = fetcherDataToProps(fetcherData as any)
+
+ // Render in browser:
+ return (
+
+
+
+ )
+}
+
+// -- Server --
+
+if (isServer) {
+ const fetcherData = use(queryClient.fetchQuery(queryConfig))
+ const routeDataProps = fetcherDataToProps(fetcherData)
+ const dehydratedState = dehydrate(queryClient)
+
+ // Render on server:
+ return (
+
+
+
+ )
+}
+```
+
+> A lot of this is pseudo code though, if you really want to know the exact internals, check out the `UniversalRouteScreen.(web).tsx` files
+
+
+
+
+
+### Fetch data with `queryFn`
+
+You'll need to define a function to tell react-query how to fetch data. This function is responsible for fetching from a server, database, or any other source. It does not have to be an actual API call, but usually, it will be.
+
+> Your `queryFn` must:
+
+- Return a promise that resolves the data.
+- Throw an error if the data cannot be fetched.
+
+
+Example
+
+
+```tsx {2, 14} /queryFn/2 filename="getPost.fetcher.ts"
+// Define a fetcher function
+const fetchProjects = async () => {
+
+ // e.g. fetch data from an API
+ const response = await fetch('/api/projects')
+
+ // Check if the response is ok, throw error if not
+ if (!response.ok) throw new Error('Network response was not ok')
+
+ // Return the data
+ return response.json()
+}
+
+// Use your `queryFn` with react-query
+const data = useQuery({ queryKey: ['projects'], queryFn: fetchProjects })
+```
+
+
+
+
+
+### `queryKey` best practices
+
+> At its core, **TanStack Query manages query caching for you based on query keys**. Query keys **have to be an Array** at the top level, and can be as simple as an Array with a single string, or as complex as an array of many strings and nested objects. As long as the query key is serializable, and unique to the query's data, you can use it!
+>
+> -- React Query Docs
+
+The simplest form of a key is an array with constants values. For example:
+
+```tsx
+// A list of todos
+useQuery({ queryKey: ['todos'], ... })
+
+// Something else, whatever!
+useQuery({ queryKey: ['something', 'special'], ... })
+```
+
+Often, your `queryFn` will take some input, like a `slug` or `id`, and you'll want to refetch the query when that input changes. In this case, you can use the input as part of the query key:
+
+```tsx
+// An individual todo
+useQuery({ queryKey: ['todo', 5], ... })
+
+// An individual todo in a "preview" format
+useQuery({ queryKey: ['todo', 5, { preview: true }], ...})
+
+// A list of todos that are "done"
+useQuery({ queryKey: ['todos', { type: 'done' }], ... })
+```
+
+
+
+### Invalidating + refetching queries
+
+**Invalidating queries** in React Query **means telling the library to refetch data**, which is necessary when the underlying data may have changed. React Query offers several ways to invalidate queries, such as directly through the queryClient or as a side effect of mutations.
+
+```tsx
+// Invalidate every query with a key that starts with `todos`
+queryClient.invalidateQueries({ queryKey: ['todos'] })
+
+// Invalidate a specific query
+queryClient.invalidateQueries({ queryKey: ['todo', 5] })
+```
+
+A common practice is to **refetch queries after updating data**. This can be done by using the `onSuccess` callback of a mutation:
+
+```tsx {4}
+const mutation = useMutation(createTodo, {
+ ...,
+ // Refetch the query with key ['todos'] after the mutation succeeds
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] })
+})
+```
+
+
+
+### Update data with `useMutation`
+
+
+
+**Mutations** are used to send data to an API. They are similar to queries, but instead of fetching data, they send data to the server. React Query provides a `useMutation` hook to handle mutations.
+
+```tsx {3, 9}
+const mutation = useMutation(createTodo, {
+ // The mutation function
+ mutationFn: (newTodo) => fetch('/api/todos', { method: 'POST', body: JSON.stringify(newTodo) }),
+ // Invalidate the 'todos' query after the mutation succeeds
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] })
+})
+
+// Call the mutation function
+mutation.mutate({ title: 'New todo' }) // <- Data will be passed as the `newTodo` arg
+```
+
+
+
+### Workspace recommendations
+
+Like our resolvers and data-bridges, we think it's best to define your custom and/or third-party data-fetching logic in files on the workspace level.
+
+> This will help **keep your feature folders clean, focused, portable and reusable across different projects**:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## GraphQL vs. API routes
+
+
+
+We recommend you use GraphQL for your data-fetching logic, as it provides a more flexible and efficient way to fetch data in each environment. For example, during server-side rendering (SSR), it's quite handy to just be able to call the executable schema.
+
+We do recommend using API routes instead when mutating / updating data. Though here too, GraphQL can be used if you prefer.
+
+
+
+### Why GraphQL for data-fetching?
+
+> As mentioned before, the 'G' in GREEN-stack stands for GraphQL. But why do we recommend it?
+
+GraphQL may look complex, but when simplified, you can get all of the benefits without said complexity:
+
+- **Type safety**: Your input and output types are automatically validated and inferred.
+- **Self-documenting**: Your API is automatically documented in `schema.graphql`.
+
+> If you don't need to worry much about the complexity of setting up and using GraphQL, it's similar to using tRPC.
+
+---
+
+It's important to note that there are some common footguns to avoid when using GraphQL:
+
+
+Why avoid a traversible graph?
+
+
+> If you don't know yet, the default way to use GraphQL is to expose many different domains as a graph.
+
+Think of a graph based API as each data type being a node, and each relationship between them being an edge. Without limits you could infinitely traverse this graph. Such as a `User` having `Post` nodes, which in themselves have related `Reply` data, which have related `User` data again. You could query all of that in one go.
+
+That might *sound* great, but it can lead to problems like:
+- **Performance**: If not limited, a single query could request so much data it puts a severe strain on your server.
+- **Scraping**: If you allow deep nesting, people can easily scrape your entire database.
+
+> Instead of exposing your entire database as a graph, we recommend you use GraphQL in a more functional "Remote Procedure Call" (RPC) style.
+
+Only when actually offering GraphQL as a public API to third party users to integrate with you, do we recommend a graph / domain style GraphQL API.
+
+
+
+Why RPC-style GraphQL?
+
+
+If you're not building a public API, you should shape your resolvers in function of your front-end screens instead of your domain or database collections. This way, you can fetch all the data required for one screen in one go, without having to call multiple resolvers or endpoints.
+
+> To illustrate RPC-style queries in your mind, think of `getDashboardData()` vs. having to call 3 separate `Orders()`, `Products()`, `Transactions()` type resolvers to achieve the same thing.
+
+When used in this manner, quite similar to tRPC, it remains the best for initial data-fetching. Though mutating data might be better served as a tRPC call or API route POST / PUT / DELETE request.
+
+We think GraphQL is great for fetching data, but not always the best for updating data. Especially when you need to handle complex forms and file uploads.
+
+You might therefore want to handle mutations of data in a more traditional REST-ful way, using tRPC or just plain old API routes.
+
+
+
+
+## Further reading
+
+From our own docs:
+- [Single Sources of Truth](/single-sources-of-truth)
+- [Data Resolvers](/data-resolvers)
+
+Relevant external docs:
+- [React Query](https://tanstack.com/query/latest/)
+- [GraphQL](https://graphql.org/learn/)
diff --git a/apps/docs/pages/data-resolvers.mdx b/apps/docs/pages/data-resolvers.mdx
new file mode 100644
index 0000000..de53e22
--- /dev/null
+++ b/apps/docs/pages/data-resolvers.mdx
@@ -0,0 +1,538 @@
+import { FileTree, Steps } from 'nextra/components'
+import { Image } from '@app/primitives'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+
+
+ ## Intro - Resolvers and APIs
+
+
+
+
+# Building a flexible API
+
+```shell
+@app-core
+ └── /resolvers/... # <- Reusable back-end logic goes here
+```
+
+> To get an idea of API design in portable workspaces, inspect the file tree below:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+This doc focuses on creating APIs that:
+
+- Are *self-documenting*, *easy to use, reuse and maintain*
+- **Keep input + output *shape, validation and types* in sync automatically**
+- Portable as part of a fully copy-pasteable feature
+
+**You'll learn to create an API that can be:**
+
+- ✅ Reused as functions / promises anywhere else in your back-end
+- ✅ A Next.js API route handler
+- ✅ An RPC style GraphQL resolver
+
+
+
+## Using the API generator
+
+Creating a new API that checks all the above boxes can involve a lot of boilerplate.
+
+To avoid that + have this boilerplate code generated, you can use the following command:
+
+```shell
+npm run add:resolver
+```
+
+This will ask you a few questions about your new API, and then generate + link the necessary files for you.
+
+```shell {4}
+>>> Modify "your universal app" using custom generators
+
+? Where would you like to add this resolver? # (use arrow keys)
+❯ features/@app-core -- importable from: '@app/core'
+ features/@some-feature -- importable from: '@app/some-feature'
+ features/@other-feature -- importable from: '@app/other-feature'
+```
+
+`⬇⬇⬇`
+
+```shell {9, 10, 11, 12}
+>>> Modify "your universal app" using custom generators
+
+? Where would you like to add this resolver? # -> features/@app-core
+? How will you name the resolver function? # -> getExampleData
+? Optional description: What will the resolver do? # -> Get some test data
+? What else would you like to generate? # -> GraphQL Resolver, GET & POST API Routes
+? Which API path would you like to use? # -> /api/example/[slug]
+
+>>> Changes made:
+ • /features/app-core/schemas/getExampleData.bridge.ts (add)
+ • /features/app-core/resolvers/getExampleData.resolver.ts (add)
+ • /features/app-core/routes/api/example/[slug]/route.ts (add)
+
+>>> Success!
+```
+
+To explain what happens under the hood, have a look at the following steps. It's what the generator helps you avoid doing manually:
+
+
+
+## Manually add a new API
+
+> It's worth noting that it's fine to reuse the Next.js or Expo ways of working to create APIs. We just provide a more portable and structured way to do so:
+
+
+
+### Start from a DataBridge
+
+
+
+
+
+
+
+
+
+
+
+
+Schemas serve as the single source of truth for your data shape. But what about API shape?
+
+You can combine input and output schemas into a `bridge` file to serve as the single source of truth for your API resolver. You can use `createDataBridge` to ensure all the required fields are present:
+
+```ts {6, 8} /createDataBridge/1,3 filename="updatePost.bridge.ts"
+import { Post } from '../schemas/Post.schema'
+import { createDataBridge } from '@green-stack/schemas/createDataBridge'
+
+/* -- Bridge ------------- */
+
+export const updatePostBridge = createDataBridge({
+ // Assign schemas
+ inputSchema: Post.partial(), // You can create, reuse or extend schemas here if needed
+ outputSchema: Post,
+
+ // Used for GraphQL defs
+ resolverName: 'updatePost',
+ resolverArgsName: 'PostInput',
+
+ // API config
+ apiPath: '/api/posts/[slug]/update',
+ allowedMethods: ['POST', 'GRAPHQL'],
+})
+```
+
+> Don't worry too much about the GraphQL and API route config for now. We'll get to that soon.
+
+Think of a **"Databridge"** as a literal *bridge between the front and back-end*.
+
+It's a metadata object you can use from either side to provide / transform / extract:
+
+- ✅ **Route handler args** from request params / body
+- ✅ Input and output **types + validation + defaults**
+- ✅ GraphQL **schema definitions** for `schema.graphql`
+- ✅ The query string to call our GraphQL API with
+
+*There's two reasons we suggest you define this "DataBridge" in a separate file*:
+
+- 1. **Reusability**: If kept separate from business logic, you can reuse it in both front and back-end code.
+- 2. **Consistency**: Predicatable patterns make it easier to build automations and generators around them.
+
+> For this reason, we suggest you add `.bridge.ts` to your bridge filenames.
+
+### Add your business logic
+
+
+
+
+
+
+
+
+
+
+
+
+
+Let's use the data bridge we just created to bundle together the input and output types with our business logic.
+
+You can do so by creating a new resolver file and passing the bridge as the final arg to `createResolver` at the end. The first argument is your resolver function containing your business logic:
+
+```ts {7, 17, 22} /updatePostBridge/ /createResolver/1,3 filename="updatePost.resolver.ts" copy
+import { createResolver } from '@green-stack/schemas/createResolver'
+import { updatePostBridge } from './updatePost.bridge'
+import { Posts } from '@db/models'
+
+/** --- updatePost() ---- */
+/** -i- Update a specific post. */
+export const updatePost = createResolver(async ({
+ args, // <- Auto inferred types (from 'inputSchema')
+ context, // <- Request context (from middleware)
+ parseArgs, // <- Input validator (from 'inputSchema')
+ withDefaults, // <- Response helper (from 'outputSchema')
+}) => {
+
+ // Validate input and apply defaults, infers input types as well
+ const { slug, ...postUpdates } = parseArgs(args)
+
+ // -- Context / Auth Guards / Security --
+
+ // e.g. use the request 'context' to log out current user
+ const { user } = context // example, requires auth middleware
+
+ // -- Business Logic --
+
+ // e.g. update the post in the database
+ const updatedPost = await Posts.updateOne({ slug }, postUpdates)
+
+ // -- Respond --
+
+ // Typecheck response and apply defaults from bridge's outputSchema
+ return withDefaults({
+ slug,
+ title,
+ content,
+ })
+
+}, updatePostBridge)
+```
+
+> You pass the bridge (☝️) as the second argument to `createResolver()` to:
+- 1️⃣ infer the input / arg types from the bridge's `inputSchema`
+- 2️⃣ enable `parseArgs()` and `withDefaults()` helpers for validation, hints + defaults
+- 3️⃣ automatically extract the request context whether in an API route / GraphQL resolver / ...
+
+> **The resulting function can be used as just another async function** anywhere in your back-end.
+
+> The difference with a regular function, since the logic is bundled together with its data-bridge / input + output metadata, is that we can easily transform it into APIs:
+
+### Turn resolver into an API route handler
+
+> We recommend workspaces follow Next.js API route conventions. This is so our scripts can automatically re-export them to the `@app/next` workspace later.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+You can create a new API route by exporting a `GET` / `POST` / `UPDATE` / `DELETE` handler assigned to a `createNextRouteHandler()` that wraps your "bridged resolver":
+
+```ts /createNextRouteHandler/1,3 /updatePost/4 filename="@some-feature / routes / api / posts / [slug] / update / route.ts"
+import { updatePost } from '@app/resolvers/updatePost.resolver'
+import { createNextRouteHandler } from '@green-stack/schemas/createNextRouteHandler'
+
+/* --- Routes ------------ */
+
+export const UPDATE = createNextRouteHandler(updatePost)
+// Automatically extracts (☝️) args from url / search params
+// based on the zod 'inputSchema'
+
+// If you want to support e.g. POST (👇), same deal (checks request body too)
+export const POST = createNextRouteHandler(updatePost)
+```
+
+What `createNextRouteHandler()` does under the hood:
+- 1. extract the input from the request context
+- 2. validate it
+- 3. call the resolver function with the args (and e.g. token / session / request context)
+- 4. return the output from your resolver with defaults applied
+
+> Check [Next.js Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) to understand supported exports (like `GET` or `POST`) and their options.
+
+> Restart your dev server or run `npm run link:routes` to make sure your new API route is available.
+
+### GraphQL, without the hassle
+
+
+
+API routes are fine, but we think GraphQL is even better, **if you don't have to deal with the hassle of managing it.** So we made it quite easy to enable GraphQL for your resolvers. The flow is quite similar.
+
+*In the same file*, add the following:
+
+```ts {3, 11} filename="features / @app-core / routes / api / health / route.ts"
+import { updatePost } from '@app/resolvers/updatePost.resolver'
+import { createNextRouteHandler } from '@green-stack/schemas/createNextRouteHandler'
+import { createGraphResolver } from '@green-stack/schemas/createGraphResolver'
+
+/* --- Routes ------------ */
+
+// exports of `GET` / `POST` / `PUT` / ...
+
+/* --- GraphQL ----------- */
+
+export const graphResolver = createGraphResolver(updatePost)
+// Automatically extracts input (☝️) from graphql request context
+```
+
+After exporting `graphResolver` here, restart the dev server or run `npm run build:schema` manually.
+
+This will:
+- 1. pick up the `graphResolver` export
+- 2. put it in our list of graphql compatible resolvers at `resolvers.generated.ts` in `@app/registries`
+- 3. recreate `schema.graphql` from input & output schemas from registered resolvers
+
+You can now check out your GraphQL API playground at [/api/graphql](http://localhost:3000/api/graphql)
+
+
+
+### Optional - Add a universal fetcher
+
+> We'll mostly be using fetchers when providing data to our app screens or routes. You can skip this step as it will be covered on the next page as well.
+
+
+How to turn a DataBridge into a fetcher?
+
+---
+
+One reason we recommed GraphQL is that it's easier to make a universal fetcher that:
+
+- can run on the server, browser or mobile
+- automatically builds the query string from the input and output schemas
+- is auto typed if using the bridge to provide the query
+- is auto typed with `gql.tada` if a custom query is used (alt)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+The easiest way to create a fetcher is to use the `bridgedFetcher()` helper:
+
+```ts {3} /bridgedFetcher/3 filename="updatePost.query.ts"
+import { updatePostBridge } from './updatePost.bridge'
+// ☝️ Reuse your data bridge
+import { bridgedFetcher } from '@green-stack/schemas/bridgedFetcher'
+// ☝️ Universal graphql fetcher that can be used in any JS environment
+
+/* --- updatePostFetcher() --------- */
+
+export const updatePostFetcher = bridgedFetcher(updatePostBridge)
+```
+
+This will automatically build the query string with all relevant fields from the bridge.
+
+To write a custom query with only certain fields, you can use our `graphql()` helper *with* `bridgedFetcher()`:
+
+```ts {3, 11, 21, 31, 41} filename="updatePost.query.ts"
+import { ResultOf, VariablesOf } from 'gql.tada'
+// ☝️ Type helpers that interface with the GraphQL schema
+import { graphql } from '../graphql/graphql'
+// ☝️ Custom gql.tada query builder that integrates with our types
+import { bridgedFetcher } from '@green-stack/schemas/bridgedFetcher'
+// ☝️ Universal graphql fetcher that can be used in any JS environment
+
+/* --- Query ----------------------- */
+
+// VSCode and gql.tada will help suggest or autocomplete thanks to our schema definitions
+export const updatePostQuery = graphql(`
+ query updatePost ($updatePostArgs: UpdatePostInput) {
+ updatePost(args: $updatePostArgs) {
+ slug
+ title
+ body
+ }
+ }
+`)
+
+// ⬇⬇⬇ automatically typed as ⬇⬇⬇
+
+// TadaDocumentNode<{
+// updatePost(args: Partial): {
+// slug: string | null;
+// title: boolean | null;
+// body: boolean | null;
+// };
+// }>
+
+// ⬇⬇⬇ can be turned into reusable types ⬇⬇⬇
+
+/* --- Types ----------------------- */
+
+export type UpdatePostQueryInput = VariablesOf
+
+export type UpdatePostQueryOutput = ResultOf
+
+/* --- updatePostFetcher() --------- */
+
+export const updatePostFetcher = bridgedFetcher({
+ ...updatePostBridge, // <- Reuse your data bridge ...
+ graphqlQuery: updatePostQuery, // <- ... BUT, use our custom query
+})
+```
+
+> Whether you use a custom query or not, you now have a fetcher that:
+
+- ✅ Uses the executable graphql schema serverside
+- ✅ Can be used in the browser or mobile using fetch
+
+
+
+
+
+
+
+## Why GraphQL?
+
+> As mentioned before, the 'G' in GREEN-stack stands for GraphQL. **But why do we recommend it?**
+
+GraphQL may look complex, but when simplified, you can get most benefits without much complexity:
+
+- **Type safety**: Your input and output types are automatically validated and inferred. (from bridge or [gql.tada](https://gql-tada.0no.co/get-started/#a-demo-in-128-seconds))
+- **Self-documenting**: The API is automatically documented in `schema.graphql` + introspection.
+
+> If you don't need to worry much setting up and consuming GraphQL, it's quite similar to using tRPC.
+
+---
+
+It's important to note that there are some common footguns to avoid when using GraphQL:
+
+
+Why avoid a traversible graph?
+
+
+> If you don't know yet, the default way to use GraphQL is to expose many different domains as a graph.
+
+Think of a graph based API as each data type being a node, and each relationship between them being an edge. Without limits you could infinitely traverse this graph. Such as a `User` having `Post` nodes, which in themselves have related `Reply` data, which have related `User` data again. You could query all of that in one go.
+
+That might *sound* great, but it can lead to problems like:
+- **Performance**: If not limited, a single query could request so much data it puts a severe strain on your server.
+- **Scraping**: If you allow deep nesting, people might more easily scrape your entire database.
+
+> Instead of exposing all of your data as a graph, we recommend you use GraphQL in a more functional "Remote Procedure Call" (RPC) style.
+
+Only when actually offering GraphQL as a public API to third party users to integrate with you, do we recommend a graph / domain style GraphQL API.
+
+
+
+Why RPC-style GraphQL?
+
+
+If you're not building a public API, you should shape your resolvers in function of your front-end screens instead of your domain or database collections. This way, you can fetch all the data required for one screen in one go, without having to call multiple resolvers or endpoints.
+
+> To illustrate RPC-style queries in your mind, think of `getDashboardData()` vs. having to call 3 separate `Orders()`, `Products()`, `Transactions()` type resolvers to achieve the same thing.
+
+When used in this manner, quite similar to tRPC, it remains the best for initial data-fetching. Though mutating data might be better served as a tRPC call or API route POST / PUT / DELETE request.
+
+We think GraphQL is great for fetching data, but not always the best for updating data. Especially when you need to handle complex forms and file uploads.
+
+You might therefore want to handle mutations of data in a more traditional REST-ful way, using tRPC or just plain old API routes / handlers.
+
+
+
+
+## Securing your API
+
+By default, any API generated by the `add:resolver` command is open to the public. Consider whether this is the right choice for that specific API.
+
+To prevent unauthorized acces, you likely want to:
+- expand the [Next.js Middleware](https://nextjs.org/docs/pages/building-your-application/routing/middleware)
+- or **add auth checks in your business logic**
+
+> We offer some solutions to help here but do not provide a default for you. More info on this in our recommended [Auth Plugins](TODO) in the sidebar.
+
+### Using the request context
+
+Our standard middleware adds any JSON serializable request context (like auth headers) to the `context` object in your resolver utils, e.g.:
+
+```ts filename="updateProfile.resolver.ts"
+export const updateProfile = createResolver(async ({
+ args,
+ context, // <- Request context (from middleware)
+ parseArgs,
+ withDefaults,
+}) => {
+ // e.g. use the request 'context' to log out current user
+ const { req, userId } = context
+
+ // ...
+
+ if (!userId) throw new Error('Unauthorized')
+
+ // ...
+
+}, updateProfileBridge)
+```
+
+This could be a good way to check for authorization in your resolver logic itself vs relying solely on middleware.
+
+### Marking fields as sensitive
+
+```ts
+ password: z.string().sensitive()
+```
+
+In your schemas, you can mark fields as sensitive using `.sensitive()`. This will:
+
+- Exclude the field from appearing in the GraphQL schema, introspection or queries
+- Mark the field as strippable in API resolvers / responses (*)
+- Mark the field with `isSensitive: true` in schema introspection
+
+> * 'Strippable' means when using either `withDefaults()` OR `applyDefaults()` / `formatOutput()` with the `{ stripSensitive: true }` option as second argument. If none of these are used, the field will still be present in API route handler responses, but not GraphQL.
+
+
+
+## Further reading
+
+From our own docs:
+- [Single Sources of Truth](/single-sources-of-truth)
+- [Universal Data Fetching](/data-fetching)
+
+Relevant external docs:
+- [GraphQL](https://graphql.org/learn/)
+- [Next.js Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers)
+- [GraphQL Resolvers](https://www.apollographql.com/docs/apollo-server/data/resolvers/)
+- [GraphQL Schema](https://www.apollographql.com/docs/apollo-server/schema/schema/)
+
+
+
+
diff --git a/apps/docs/pages/form-management.mdx b/apps/docs/pages/form-management.mdx
new file mode 100644
index 0000000..9f36114
--- /dev/null
+++ b/apps/docs/pages/form-management.mdx
@@ -0,0 +1,361 @@
+import { FileTree, Steps, Cards } from 'nextra/components'
+import { Image } from '@app/primitives'
+
+
+
+# Schema based Form management
+
+```tsx copy
+import { useFormState } from '@green-stack/forms/useFormState'
+```
+
+
+
+
+
+
+
+
+
+
+
+
+A major goal of this starterkit is to use `zod` schemas as the ultimate source of truth for your app's data shapes, validation and types. *We've extended this concept to include form management as well:*
+
+
+
+## `useFormState()`
+
+```tsx {10} filename="useSomeFormState.ts"
+// Define a Zod schema for your form state (or use an existing one)
+const SomeFormState = schema('SomeFormState', {
+ name: z.string(),
+ age: z.number().optional(),
+ email: z.string().email(),
+ birthDate: z.date().optional(),
+})
+
+// Create a set of form state utils to use in your components
+const formState = useFormState(SomeFormState, {
+ initialValues: { ... },
+ // ... other options
+})
+```
+
+`formState.values` is typed according to the Zod schema you provided as the first argument:
+
+```tsx
+formState.values.name // string
+formState.values.age // number | undefined
+formState.values.email // string
+formState.values.birthDate // Date | undefined
+```
+
+
+
+### defaults and `initialValues`
+
+You can provide initial values through the `initialValues` option:
+
+```tsx {2}
+const formState = useFormState(SomeFormSchema, {
+ initialValues: {
+ name: 'Thorr Codinsonn',
+ email: 'thorr@codinsonn.dev',
+ },
+})
+
+formState.values.name // 'Thorr Codinsonn'
+formState.values.email // 'thorr@codinsonn.dev'
+```
+
+Alternatively, your schema can define default values as well:
+
+```tsx /.default/
+const SomeFormSchema = schema('SomeFormSchema', {
+ name: z.string().default('Thorr Codinsonn'),
+ email: z.string().email().default('thorr@codinsonn.dev'),
+})
+
+const formState = useFormState(SomeFormSchema)
+
+formState.values.name // 'Thorr Codinsonn'
+formState.values.email // 'thorr@codinsonn.dev'
+```
+
+
+
+### Retrieving and updating values
+
+You can use `formState.getValue('some-key'){:tsx}` to get a specific value from the form state. The `'some-key'` argument is any key in the Zod schema you provided as stateSchema and the available keys will he hinted by your IDE.
+
+```tsx
+formState.getValue('name') // string
+// ?^ Hinted keys: 'name' | 'email' | 'age' | 'birthDate'
+```
+
+Updating the formState values can similarly be done in two ways:
+
+```tsx
+// Update a single value in the form state by its hinted key
+formState.handleChange('email', 'thorr@fullproduct.dev') // OK
+formState.handleChange('age', 'thirty two') // ERROR (non a number)
+```
+
+```tsx
+// Update multiple values in the form state by passing an object with keys and values
+formState.setValues({
+ name: 'Thorr', // OK
+ email: 'thorr@fullproduct.dev', // OK
+ age: 'some-string', // ERROR: Type 'string' is not assignable to type 'number'
+})
+```
+
+Typescript and your IDE will help you out with the available keys and allowed values through hints and error markings if you try to set a value that doesn't match the Zod schema.
+
+
+
+### Validation and error handling
+
+Call `formState.validate(){:tsx}` to validate current state values against the Zod schema:
+
+```tsx
+const isValid = formState.validate() // true | false
+```
+
+Validating the formstate will also update the `formState.errors` object with any validation errors.
+
+```tsx
+formState.errors
+// {
+// name: ['Required'],
+// email: ['Invalid email']
+// }
+```
+
+You could also trigger validation on any change to the form state by passing the `validateOnChange` option:
+
+```tsx {2}
+const formState = useFormState(SomeFormSchema, {
+ validateOnChange: true,
+})
+```
+
+You can manually update or clear the errors object:
+
+```tsx
+formState.updateErrors({ name: ['Required'] })
+
+// Clear all errors
+formState.clearErrors()
+```
+
+`clearErrors()` is essentially the same thing as clearing errors manually with:
+
+```tsx
+// e.g. Clear all error messages by passing an empty object or empty arrays
+formState.updateErrors({
+ username: [],
+ email: [],
+ password: [],
+ twoFactorCode: [],
+})
+```
+
+
+
+#### Custom Errors
+
+You can provide custom error messages by passing the `message` prop in your form schema:
+
+```ts
+age: z.number({ message: 'Please provide a number' })
+// Throws => ZodError: [{ message: "Please provide a number", ... })
+```
+
+You can provide **custom error messages** for specific validations as well:
+
+```ts
+const SomeFormSchema = schema('SomeFormSchema', {
+
+ age: z.number().min(10, { message: 'You must be at least 18 years old' }),
+ // Throws => ZodError: [{ message: "You must be at least 18 years old", ... })
+
+ password: z.string().len(12, { message: 'Passwords must be at least 12 characters long' }),
+ // Throws => ZodError: [{ message: "Passwords must be at least 12 characters long", ... })
+
+})
+```
+
+
+
+## Universal Form Components
+
+Our [full-product.dev](https://fullproduct.dev) starterkit comes with some (minimally styled) universal form components out of the box.
+
+You can see them in the sidebar under `@app-core/forms`:
+
+
+
+
+
+
+
+
+
+
+
+
+> These are built with `useFormState` in mind and based on Nativewind-compatible [react-native-primitives](https://rn-primitives.vercel.app/).
+
+
+
+### Integrating with Components
+
+`formState` exposes a few utility functions to make it easy to integrate with form components:
+
+- ✅ `value` props
+- ✅ `onChange` props
+- ✅ `hasError` prop
+
+
+
+#### `getInputProps(...)`
+
+You can use `formState.getInputProps()` to easily integrate your form state with React components that:
+
+- Accept a `value` prop
+- Accept an `onChange` prop
+
+```tsx {6, 15}
+
+
+
+```
+
+
+
+#### `getTextInputProps(...)`
+
+`formState.getTextInputProps()` is a replacement for `getInputProps()` with a `TextInput` component:
+
+- Uses `onChangeText` instead of `onChange`
+
+```tsx {3, 8}
+
+
+
+```
+
+
+
+## Best practices
+
+### `formState` as props
+
+If a child component requires access to the form state, you'll save a bunch of time and lines of code by passing the `formState` object as a prop:
+
+```tsx
+
+```
+
+This is typically handy when:
+
+- ✅ You're doing modal forms
+- ✅ You have a clear logic and view layer split
+- ✅ You're trying to avoid using global state
+
+
+
+### Flags to track changes
+
+Sometimes you need to execute code when your form state changes.
+
+It's a good idea to use the flags provided in `formState` for this:
+
+```tsx
+// Whether the form is valid, even before calling .validate()
+const isValid = formState.isValid
+
+// Whether the form had changes since the last save
+const isUnsaved = formState.isUnsaved
+
+// Whether the form is in its default state (from initialValues or schema defaults)
+const isDefaultState = formState.isDefaultState
+```
+
+
+
+### Triggering effects based on form state changes
+
+`formState` has a `valuesKey` property that changes every time the form state changes. This can be used to trigger effects when the form state changes:
+
+```tsx
+useEffect(() => {
+ // Do something when the form state changes
+}, [formState.valuesKey])
+```
+
+
+
+## API Reference
+
+| Property | Description |
+|---------------------------|---------------------------------------------------------------------------------------------------|
+| `values` | The current values of the form |
+| `setValues` | Sets the entire form state to the provided values |
+| `getValue` | Gets the form value for the provided field key |
+| `setValue: handleChange` | Sets the form value for the provided field key |
+| `handleChange` | Sets the form value for the provided field key |
+| `getChangeHandler` | Gets the change handler for the provided field key |
+| `validate` | Validates the form state, sets errors if not, and returns whether it is valid or not |
+| `isValid` | Whether the form is valid |
+| `isUnsaved` | Whether the form is unsaved |
+| `isDefaultState` | Whether the form is in its default state |
+| `errors` | The current errors of the form |
+| `updateErrors` | Sets the errors for the form |
+| `getErrors` | Gets the errors for the provided field key |
+| `hasError` | Whether the provided field key has an error |
+| `clearErrors` | Clears all errors until validated again |
+| `clearForm` | Clears the form values, applying only the schema defaults |
+| `resetForm` | Resets the form to its original state using initialValues & schema defaults |
+| `getInputProps` | The props to add to an input to manage its state |
+| `getTextInputProps` | The props to add to a text input to manage its state, uses `onTextChange` instead |
+| `getNumberTextInputProps` | The props to add to a number input to manage its state, uses `onTextChange` instead |
+| `valuesKey` | The key of the current form values, good for use in hook dependencies to trigger recalculations |
+
+
+
+## Further reading
+
+From our own docs:
+- [Single Sources of Truth](/single-sources-of-truth) to base your form state on
+- [Data Resolvers](/data-resolvers) to send your form state to
+
+Relevant external docs:
+- [react-native-primitives](https://rn-primitives.vercel.app/)
diff --git a/apps/docs/pages/generators.mdx b/apps/docs/pages/generators.mdx
new file mode 100644
index 0000000..f4aeadc
--- /dev/null
+++ b/apps/docs/pages/generators.mdx
@@ -0,0 +1,61 @@
+import { Image } from '@app/primitives'
+import { FileTree } from 'nextra/components'
+import { TitleWrapper } from '@app/docs/components/Hidden'
+
+
+ ## Available Generators
+
+
+
+
+# Codegen with Turbo Generators
+
+Our Starterkit goes beyond the setup by helping you skip boilerplate code through generators:
+
+- [`add-workspace`](@green-stack-core/generators/add-workspace)
+- [`add-script`](@green-stack-core/generators/add-script)
+- [`add-schema`](@green-stack-core/generators/add-schema)
+- [`add-route`](@green-stack-core/generators/add-route)
+- [`add-resolver`](@green-stack-core/generators/add-resolver)
+- [`add-generator`](@green-stack-core/generators/add-generator)
+- [`add-form`](@green-stack-core/generators/add-form)
+
+## Using generators programmatically
+
+### Bypassing prompts in the terminal
+
+Any generator in the project can be called from the terminal with pre-filled answers to the prompts. This will bypass the question being asked.
+
+```md
+npx turbo gen --args ...
+```
+
+This also works with some of the npm scripts predefined in the project, but you'll need to pass the `--` argument to separate the npm script arguments from the npm command line arguments, e.g.:
+
+```md
+npm run add:schema -- --args
+```
+
+Take note that the order of the arguments is important.
+
+Pass them in the order they are defined as prompts in the generator / asked without providing them.
+
+If you pass too little arguments, the generator will simply ask you to answer the rest of the questions.
+
+> You can also pass `_` instead of an answer to an argument to still have that specific question asked, e.g:
+
+```md
+npx turbo gen --args _ ...
+```
+
+```md
+npm run add:schema -- --args 'packages/@app-core' _ 'Some New Schema'
+
+>>> ? What is the name of the schema?
+```
diff --git a/apps/docs/pages/index.mdx b/apps/docs/pages/index.mdx
new file mode 100644
index 0000000..868e599
--- /dev/null
+++ b/apps/docs/pages/index.mdx
@@ -0,0 +1,879 @@
+import { FileTree } from 'nextra/components'
+import { View, Image } from '@app/primitives'
+import { ImageZoom } from 'nextra/components'
+
+
+
+# GREEN stack quickstart
+
+
+
+
+
+> **Welcome to FullProduct.dev!** 🙌
+> This guide will help set up your Web, iOS and Android app up quickly. It will also introduce you to write-once components that render on all platforms, powered by GraphQL, React-Native, Expo and Next.js
+
+This quickstart covers a few topics:
+
+- **[Using the Starterkit](#up-and-running-in-no-time)** and **[Project Architecture](#monorepo-architecture)**
+- **[The GREEN stack + write once, render anywhere](#the-green-stack)** way of working
+- **[Zod for Single sources of truth](#zod-for-single-sources-of-truth)** - Defining **portable data shapes** for UIs, APIs and beyond
+- **[Universal Data Fetching](#universal-routes--data-fetching)** - Using `react-query` and e.g. GraphQL (without the hassle)
+- Git / PR based plugins to **[Pick your own Auth / DB / Mail / Storage / ...](#powerful-results-)**
+
+## Start for Web *+* App Stores with [FullProduct.dev](https://fullproduct.dev) ⚡️
+
+**Fork or generate** a new repo from the [green-stack-starter](https://github.com/FullProduct-dev/green-stack-starter-demo) repo and **include all branches** to enable plugins later.
+
+Github will generate a copy of the template repo to customize.
+It comes out of the box with setup for:
+
+- ✅ **Next.js web app** (file based app dir routing, SSR, SSG, ...)
+- ✅ **Expo mobile app** (android & iOS with expo-router and react-native)
+- ✅ **Monorepo setup** + `@app/core` workspace to write reusable code for both
+- ✅ Both a **REST & GraphQL API**, with apollo server & next.js API routes
+- ✅ **Generators** and **automation** to generate files, API's & component docs
+- ✅ **Docs** and **recommended way of working** for the starterkit and its features and plugins
+
+> Optionally, have a look at some plugin branches:
+
+- Github actions for automatic mobile deployments
+- Linting and prettifying your code
+- Automatic interactive docs generation for features you develop
+- Email, Authentication, Payments, Storage and more 🙌
+
+### Up and running in no time
+
+**When you're ready to start developing, run `npm install`, followed by:**
+
+```shell copy
+npm run dev
+```
+
+> Check out the web version on [localhost:3000](https://localhost:3000)
+
+[](https://fullproduct.dev/demos)
+
+> To test the Mobile version on iOS or Android, download the [Expo Go](https://expo.dev/go) app on your device.
+> You may need to run `npm -w @app/expo start` **once** to sign up / in to your expo account.
+
+### Monorepo Architecture
+
+Your main entrypoint will be the `@app/core` package in our monorepo setup:
+
+```shell {8}
+your-project/
+
+ └── /apps/
+ └── /expo/... # <- Expo-Router for iOS & Android
+ └── /next/... # <- Next.js App Router (SSR, SSG, API routes)
+
+ └── /features/...
+ └── /@app-core/... # <- Reusable core features, resolvers, UI and screens
+
+ └── /packages/...
+ └── /@green-stack-core/... # <- Starterkit and Universal App helpers
+
+```
+
+Since the goal is to keep things as write-once as possible, we don't recommend writing code or features directly in the `@app/expo` or `@app/next` workspaces. Keep them for config and routing only.
+
+If you don't care about keeping code copy-pasteable, you obviously can write it there. Our recommended way of working is to write reusable code in the `/features/` or `/packages/` workspaces (such as `@app/core`) instead.
+
+Following this recommendation will make maximum code reuse for web and mobile easier.
+
+Workspaces in `/packages/` serve a similar purpose to `/features/`, but are more focused on plugins, utils, helpers, drivers and SDKs to enhance writing reusable features.
+
+## The GREEN stack
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+The `@green-stack/core` package helps you bring together:
+
+- ✅ **GraphQL** for a typed contract between client & API
+- ✅ **React-Native** for write-once UI
+- ✅ **Expo** for the best mobile DX and UX + App store deploys
+- ✅ **Next.js** for optimized web-vitals, API routes, SEO and SSR / SSG
+
+Combined with **Typescript, React-Query, Zod** and **gql.tada** you get a a well rounded tech stack.
+
+`@green-stack/core` has the following aliases to import from:
+
+- `@green-stack/schemas` - Toolkit around Zod schemas so you can use them as single source of truth
+- `@green-stack/styles` - Nativewind helpers like `styled()` to help build universal UI
+- `@green-stack/navigation` - Routing & fetching tools for the Next.js & Expo-Router app routers
+- `@green-stack/components` - e.g. `` component that's optimized for web and mobile
+- `@green-stack/scripts`- Workspace automation, e.g. re-export routes from features in Expo & Next
+- `@green-stack/generators` - Interactive cli tools to easily create routes, screens and API's
+
+### Recommended structure
+
+We strongly recommend **colocating code by feature or domain** instead of a typical front-end / back-end split:
+
+```shell {12, 15, 18, 20, 21, 22}
+your-project/
+
+ └── /features/...
+
+ └── /@app-core/... # <- e.g. Workspace for the "core feature" of your app
+
+ └── /constants/...
+ └── /utils/...
+
+ └── /schemas/... # Zod
+ └── /models/... # DB
+ └── /resolvers/... # Back-End Logic (*)
+
+ └── /components/... # UI
+ └── /hooks/... # Front-End Logic
+
+ └── /screens/...
+ └── HomeScreen.tsx # 👈 Start here
+
+ └── /routes/... # Auto re-exported to '@app/expo' & '@app/next'
+ └── /api/... # Turn resolvers (*) into API routes & graphql resolvers
+ └── index.tsx # 👈 e.g. uses 'HomeScreen'
+
+ └── package.json # List package deps and define name, e.g. '@app/core'
+
+ └── ... # Other config files like 'tsconfig.json'
+
+ └── /some-other-feature/...
+
+ └── /../...
+
+```
+
+> *Routes can be separated by feature / domain as well. Scripts from `@green-stack/scripts` can handle the automatic re-exporting of (api / page) routes to `@app/expo` and `@app/next` for you. - for example:
+
+```shell copy
+npm run link:routes
+```
+
+This will eventually help us keep entire features as copy-pasteable between projects as possible. More on that later in the [Core Concepts](/core-concepts) section.
+
+For now, you should start in a screen component like `HomeScreen.tsx`
+
+### Write once, render anywhere
+
+The default UI primitives to use for building Universal Apps are those that react-native comes with. Instead of using `
`, `
`, `` or ``, you instead use ``, `` and ``
+
+```typescript copy
+import { View, Text, Image } from 'react-native'
+// ☝️ Auto-transformed to 'react-native-web' in Next.js
+```
+
+### Universal Styling
+
+Our recommended way of styling cross-platform UI components is to use [Tailwind CSS](https://tailwindcss.com/) through [Nativewind](https://www.nativewind.dev/):
+
+```typescript
+import { View, Text, Image } from 'nativewind'
+// ☝️ Import from 'nativewind' instead
+```
+
+OR import them from your own predefined styled system:
+
+
+
+
+
+
+
+
+
+
+
+```tsx {12} /styled/ filename="styled.tsx"
+import { Text as RNText } from 'react-native'
+import { styled } from '@green-stack/styles'
+
+// ... other re-exported predefined styles ...
+
+/* --- Typography ------------ */
+
+export const P = styled(RNText, 'text-base')
+export const H1 = styled(RNText, 'font-bold text-2xl text-primary-100')
+export const H2 = styled(RNText, 'font-bold text-xl text-primary-100')
+export const H3 = styled(RNText, 'font-bold text-lg text-primary-100')
+// ☝️ These styles will always be applied unless overridden by the className prop
+
+```
+
+> For convenience, we've already set up an `@app/primitives` alias that points to this file for you.
+
+Usage - e.g. `HomeScreen.tsx`
+
+```typescript {5, 13, 22} /className/
+import { Image, View, H1 } from '@app/primitives'
+
+// ⬇⬇⬇
+
+
+
+/* Use the 'className' prop like you would with tailwind on the web */
+
+// ⬇⬇⬇
+
+// When rendering on Mobile:
+
+// 'px-2' -> { paddingLeft: 8, paddingRight: 8 }
+// 'max-w-[100px]' -> { maxWidth: 100 }
+// 'items-center' -> { alignItems: 'center' }
+// 'rounded-md' -> { borderRadius: 6 }
+
+// -- vs. --
+
+// When rendering on the server or browser:
+
+// 'px-2' -> padding-left: 8px; padding-right: 8px;
+// 'max-w-[100px]' -> max-width: 100px;
+// 'items-center' -> align-items: center;
+// 'rounded-md' -> border-radius: 6px;
+```
+
+> Check [nativewind.dev](https://nativewind.dev) for a deeper understanding of [Universal Styling](/write-once-styles)
+
+## Zod for Single sources of truth
+
+`@green-stack/schemas` will allow you to define any data structure with [zod](https://zod.dev), but then provide helpers to *transform* them into:
+
+- ✅ Types
+- ✅ Input validation
+- ✅ Output and React prop defaults
+- ✅ Form state hooks
+- ✅ Database models (pick your own DB)
+- ✅ GraphQL schema definition language
+- ✅ Component docs
+
+> This means you'll only need to define the shape for all these just once, using zod:
+
+### Writing portable schemas
+
+Try creating a `User` schema for example:
+
+```shell
+@app/core
+ └── /schemas/... # 💡 Keep Zod based single source of truth in '/schemas/'
+```
+
+
+
+
+
+
+
+
+
+
+
+```ts {4, 7, 11} /.min/ /.default/ /.nullish/ /.nullable/ /.optional/ filename="User.schema.ts"
+import { z, schema } from '@green-stack/schemas'
+
+// Define the shape of the user data
+export const User = schema('User', {
+ // Requires a name value (☝️) to port to other formats later, best to keep the same
+
+ // Zod can help you go even narrower than typescript
+ name: z.string().min(2), // <- e.g. Needs to be a string with at least 2 letters
+ age: z.number().min(18), // <- e.g. Age must be a number of at least 18
+
+ // Just like TS, it can help you indicate fields as optional
+ isAdmin: z.boolean().default(false), // <- Marked optional, defaults to false
+ birthdate: z.Date().nullish(), // = same as calling .nullable().optional()
+})
+```
+
+Already, our zod powered schema can act like a **single source of truth** for both **types and validation**:
+
+```ts {15, 16} /z.infer/ /?/ /| null/
+// Extract type from the schema and export it as a type alias
+export type User = z.infer
+
+// ⬇⬇⬇
+
+// {
+// name: string,
+// age: number,
+// isAdmin?: boolean,
+// birthDate?: Date | null,
+// }
+
+// ⬇⬇⬇
+
+// Usage as a type
+const newUser: User = { ... }
+```
+
+> For *validation*, you can call `.parse()` on the whole User schema:
+
+```ts /parse/1
+// Parsing will auto infer the type if valid
+const newUser = User.parse(someInput)
+
+// You can also parse an individual property by using '.shape' 👇
+
+User.shape.age.parse("Invalid - Not a number")
+// Throws => ZodError: "Expected a number, recieved a string."
+// Luckily, TS will already catch this in your editor ( instant feedback 🙌 )
+```
+
+> Check out [zod.dev](https://zod.dev) and the [Single Sources of Truth](/single-sources-of-truth) docs later for a deep of zod's typescript-first schema building abilities.
+
+To highlight the power of schemas, let's look beyond validation and types:
+
+### Build a data resolver (API route + GraphQL) with zod
+
+```shell
+@app/core
+ └── /resolvers/... # <- Write reusable back-end logic in '/resolvers/' folders
+```
+
+Let's link an Input schema and Output schema to some business logic:
+
+```ts {3, 4, 11, 12} filename="healthCheck.resolver.ts"
+/* -- Schemas ------------ */
+
+// Input validation
+export const HealthCheckInput = schema('HealthCheckInput', {
+
+ echo: z.string()
+ .default('Hello World!')
+ .describe("Will ne echo'd back in the response"), // Docs
+})
+
+// Output definition
+export const HealthCheckOutput = schema('HealthCheckOutput', {
+
+ echo: HealthCheckInput.shape.echo, // 1 of many ways to reuse defs
+
+ alive: z.boolean().default(true),
+ kicking: z.boolean().default(true),
+})
+```
+
+To be able to reuse these on the front-end later, you'll want to combine them as a "bridge":
+
+```ts {5, 10, 14} /inputSchema/ /outputSchema/ filename="healthCheck.bridge.ts"
+import { createDataBridge } from '@green-stack/schemas/createDataBridge'
+
+/* -- Bridge ------------- */
+
+export const healthCheckBridge = createDataBridge({
+ // Assign schemas
+ inputSchema: HealthCheckInput,
+ outputSchema: HealthCheckOutput,
+
+ // GraphQL config
+ resolverName: 'healthCheck',
+ resolverArgsName: 'HealthCheckInput',
+
+ // API route config
+ apiPath: '/api/health',
+ allowedMethods: ['GRAPHQL', 'GET'],
+})
+```
+
+Think of a **"Databridge"** as a literal *bridge between the front and back-end*.
+
+It's a metadata object you can use from either side to provide / transform into:
+
+- ✅ Input and output **types + validation + defaults**
+- ✅ GraphQL **schema definitions** in `schema.graphql`
+- ✅ The query string to call our GraphQL API with
+
+> It's not necessarily recommended in this specific case, but a clean split filewise coud look like this:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+> This might make more sense if you know you'll be reusing these data shapes (e.g. 'User', 'Post') outside of the context of the resolver.
+
+For now, let's just connect the bridge to our actual server-side business logic:
+
+```ts {2, 6, 18, 19, 29} /healthCheckBridge/ filename="healthCheck.resolver.ts"
+import { createResolver } from '@green-stack/schemas/createResolver'
+import { healthCheckBridge } from './healthCheck.bridge.ts'
+
+/** --- healthCheck() ---- */
+/** -i- Check the health status of the server. */
+export const healthCheck = createResolver(async ({
+ args,
+ context, // <- Request context (from middleware)
+ parseArgs, // <- Input validator (from 'inputSchema')
+ withDefaults, // <- Response helper (from 'outputSchema')
+}) => {
+
+ // Auto typed input:
+ const { echo } = args
+
+ // -- OR --
+
+ // Validate input, infer types and apply defaults
+ const { echo } = parseArgs(args)
+
+ // -- ... --
+
+ // Add business logic
+ // - e.g. log out the request 'context'?
+
+ // -- Respond --
+
+ // Typecheck response and apply defaults from bridge's outputSchema
+ return withDefaults({
+ echo,
+ alive: true,
+ // 'kicking' will be defaulted to true automatically by zod
+ })
+
+}, healthCheckBridge)
+// ☝️ Provide the bridge as the 2nd argument to:
+// - infer the types
+// - enable the parseArgs() and withDefaults() helpers
+```
+
+The resulting `healthCheck()` function can be used as just another async function anywhere in your back-end.
+
+The difference with a regular function, since the logic is now bundled together with its DataBridge / input + output metadata, is that we can easily transform it into an API route:
+
+### Creating API routes from Resolvers
+
+```shell
+@app/core
+ └── /resolvers/...
+ └── /routes/...
+ └── /api/... # <- Define API routes at this level
+```
+
+`/api/health/route.ts` ➡️ We recommend workspaces follow Next.js API route conventions. This is so our scripts can automatically re-export them to the `@app/next` workspace later.
+
+You can create a new API route by exporting a `GET` or `POST` handler assigned to a `createNextRouteHandler()` wrapping your "bridged resolver" function:
+
+```ts /createNextRouteHandler/1,3 /healthCheck/4 filename="features / @app-core / routes / api / health / route.ts"
+import { healthCheck } from '@app/resolvers/healthCheck.resolver'
+import { createNextRouteHandler } from '@green-stack/schemas/createNextRouteHandler'
+
+/* --- Routes ------------ */
+
+export const GET = createNextRouteHandler(healthCheck)
+// Automatically extracts (☝️) args from url & search params
+// based on the zod 'inputSchema'
+
+// If you want to support e.g. POST (👇), same deal (checks body as well)
+export const POST = createNextRouteHandler(healthCheck)
+```
+
+What `createNextRouteHandler()` does under the hood is extract the input from the request context, validate it, call the resolver function with the args (and e.g. token / session data) and return the output from your resolver with defaults applied.
+
+> 💡 Be sure to check [Next.js Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) later for a deeper understanding of supported exports (like `GET` or `POST`) and their options. You might also want to expand the [Next.js Middleware](https://nextjs.org/docs/pages/building-your-application/routing/middleware) or **add auth checks in your business logic** to prevent unauthorized access. We have a few **auth plugins** that can help you with this.
+
+If you've restarted your dev server or ran `npm run link:routes`, test your API at [/api/health](http://localhost:3000/api/health)
+
+### Attaching a Resolver to GraphQL
+
+API routes are fine, but we think GraphQL can be even better, ***IF you don't have to deal with the hassle of managing it.*** So we made it quite easy to enable GraphQL for your resolvers. The flow is quite similar.
+
+*In the same file*, add the following:
+
+```ts {3, 11} filename="features / @app-core / routes / api / health / route.ts"
+import { healthCheck } from '@app/resolvers/healthCheck.resolver'
+import { createNextRouteHandler } from '@green-stack/schemas/createNextRouteHandler'
+import { createGraphResolver } from '@green-stack/schemas/createGraphResolver'
+
+/* --- Routes ------------ */
+
+// exports of `GET` / `POST` / `PUT` / ...
+
+/* --- GraphQL ----------- */
+
+export const graphResolver = createGraphResolver(healthCheck)
+// Automatically extracts input (☝️) from graphql request context
+```
+
+After exporting `graphResolver` here, restart the dev server or run `npm run build:schema` manually.
+
+You can then check out your GraphQL API playground at [/api/graphql](http://localhost:3000/api/graphql)
+
+
+
+> Check [Resolvers and API's](/data-resolvers) later for a deeper understanding of how this all works under the hood.
+
+## Universal Routes + Data Fetching
+
+To fetch the right amount of data with GraphQL, we'll need to specify the right query for it.
+
+Thanks to `gql.tada`, we can write queries with hints. The args and results are also automatically typed based on the GraphQL schema the startkit automatically extracts for you:
+
+
+
+
+
+
+
+
+
+
+
+```ts {2, 4, 19, 29} /healthCheckQuery/ filename="features / @app-core / resolvers / healthCheck.query.ts"
+import { ResultOf, VariablesOf } from 'gql.tada'
+// ☝️ Type helpers that interface with the GraphQL schema
+import { graphql } from '../graphql/graphql'
+// ☝️ Custom gql.tada query builder that integrates with our types
+
+/* --- Query ----------------------- */
+
+// VSCode will help suggest or autocomplete thanks to our schema definitions
+export const healthCheckQuery = graphql(`
+ query healthCheck ($healthCheckArgs: HealthCheckInput) {
+ healthCheck(args: $healthCheckArgs) {
+ echo
+ alive
+ kicking
+ }
+ }
+`)
+
+// ⬇⬇⬇ automatically typed as ⬇⬇⬇
+
+// TadaDocumentNode<{
+// healthCheck: {
+// echo: string | null;
+// alive: boolean | null;
+// kicking: boolean | null;
+// };
+// }>
+
+// ⬇⬇⬇ can be turned into reusable types ⬇⬇⬇
+
+/* --- Types ----------------------- */
+
+export type HealthCheckQueryInput = VariablesOf
+
+export type HealthCheckQueryOutput = ResultOf
+```
+
+> Check out [gql.tada](https://gql-tada.0no.co/get-started/#a-demo-in-128-seconds) later to how to write and use GraphQL queries with typescript.
+
+You might think this is a lot of work for a simple query. However, you don't necessarily have to write these queries out yourself. Once we reuse our **`DataBridge`**, it can scaffold out the query for us:
+
+```ts {7, 8} /bridgedFetcher/1,3 /healthCheckBridge/2 filename="features / @app-core / resolvers / healthCheck.query.ts"
+import { healthCheckBridge } from './healthCheck.bridge'
+import { bridgedFetcher } from '@green-stack/schemas/bridgedFetcher'
+// ☝️ Helper to automatically create a fetcher from a DataBridge
+
+/* --- healthCheckFetcher() -------- */
+
+// Use the bridge to automatically create the fetcher function
+export const healthCheckFetcher = bridgedFetcher({
+
+ ...healthCheckBridge,
+ // ☝️ Uses the bridge to create the query and input + output types for you
+
+ // OPTIONALLY override the default query
+ graphqlQuery: healthCheckQuery,
+ // ☝️ If you only need specific fields, and want the response type to match that
+})
+```
+
+> Same file, same results, but a lot easier, right?
+
+To recap, `bridgedFetcher()` will automatically create the fetcher function from a DataBridge:
+
+- ✅ **Creates the query string**. No more manual typing, **just pass the databridge**.
+- ✅ You **can override the default query** by passing a custom `graphqlQuery`
+- ✅ **Auto infers input and output types** for the function from either the bridge or custom query
+- ✅ Resulting **fetcher function** *can be used on server, browser and mobile with `react-query`*
+
+> You've officially skipped a lot of the complexity of working with GraphQL 🙌
+
+### Fetching initial Data in Screens
+
+
+
+There are 3 environments to consider when providing dynamic data to your screens:
+
+- **Server-side rendering (SSR)** using the **[executable schema](https://the-guild.dev/graphql/tools/docs/generate-schema)**
+- **Client-side rendering (CSR)** in the browser (**hydration** or fetch)
+- **Mobile App** client in Expo (fetch only)
+
+To fetch data the same way in all three, we've written two helpers:
+- `createQueryBridge()` - Build **instructions for data-fetching with `react-query`**
+- `` - Component that **uses the bridge to fetch data** in each environment
+
+Here's how we'd build the queryBridge in the 'Home' route we set up at the start:
+
+### 1. Start with component & bridge in `/screens/` folder
+
+Think of a "`QueryBridge`" as a bridge between the route component and the data-fetching logic. It's a way to fetch data for a route, based on the route's parameters.
+
+The closest thing you could compare it to is next.js's `getServerSideProps`. Except it also works to fetch data on your Native App, not just during Web SSR or CSR:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```tsx {2, 7, 24, 30} /createQueryBridge/ /routeDataFetcher/ /serverHealth/1,3 filename="HomeScreen.tsx"
+import { createQueryBridge } from '@green-stack/navigation'
+import { healthCheckFetcher } from '@app/resolvers/healthCheck.resolver'
+import type { HydratedRouteProps } from '@green-stack/navigation'
+
+/* --- Data Fetching --------------- */
+
+export const queryBridge = createQueryBridge({
+
+ // 1. Transform the route params into things useable by react-query
+ routeParamsToQueryKey: (routeParams) => ['healthCheck', routeParams.echo],
+ routeParamsToQueryInput: (routeParams) => ({ healthCheckArgs: { echo: routeParams.echo } }),
+
+ // 2. Provide the fetcher function to be used by react-query
+ routeDataFetcher: healthCheckFetcher,
+
+ // 3. Transform fetcher output to props after react-query was called
+ fetcherDataToProps: (fetcherData) => ({ serverHealth: fetcherData?.healthCheck }),
+})
+
+// ⬇⬇⬇ Extract types ⬇⬇⬇
+
+/* --- Types ----------------------- */
+
+type HomeScreenProps = HydratedRouteProps
+
+// ⬇⬇⬇ Use fetcher data in screen component ⬇⬇⬇
+
+/* --- --------------- */
+
+const HomeScreen = (props: HomeScreenProps) => {
+
+ // Query results from 'fetcherDataToProps()' will be added to it
+ const { serverHealth } = props
+ // ☝️ Typed as {
+ // serverHealth: {
+ // echo: string,
+ // alive?: boolean,
+ // kicking?: boolean,
+ // }
+ // }
+
+ // -- Render --
+
+ return (...)
+}
+```
+
+### 2. Use bridge & component in workspace `/routes/` folder
+
+```shell
+@app/core
+ └── /screens/...
+ └── HomeScreen.ts # <- Where we've defined the data-fetching logic *and* UI
+ └── /routes/...
+ └── index.ts # <- Where we'll combine the bridge & UI component
+```
+
+Time to bring it all together by turning the HomeScreen into an actual route we can visit:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+This is where `UniversalRouteScreen` comes in to **execute each step** op the `queryBridge` **in sequence** until we get to the final props to be provided to the screen.
+
+```tsx {2, 7} /queryBridge/3 /HomeScreen/2 filename="features / @app-core / routes / index.tsx"
+import { HomeScreen, queryBridge } from '@app/screens/HomeScreen'
+import { UniversalRouteScreen } from '@app/navigation'
+
+/* --- /subpages/[slug] ----------- */
+
+export default (props) => (
+
+)
+```
+
+In the same `/routes/index.tsx` file, you can add the [Next.js routing config](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config)
+
+```tsx {} filename="features / @app-core / routes / index.tsx"
+// -i- Export any other next.js routing config here
+export const dynamic = 'auto'
+export const dynamicParams = true
+export const revalidate = false
+export const fetchCache = 'auto'
+export const runtime = 'nodejs'
+export const preferredRegion = 'auto'
+export const maxDuration = 5
+```
+
+> 💡 Check Next.js [route segment config](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config) later to understand the options you can set here.
+
+We'll be re-exporting this route segment config in the next step. We'll keep it in the same file as the main route component for colocation and enabling `@green-stack/scripts` to automatically re-export it.
+
+### 3. Reexport route file in Expo & Next.js app routers
+
+> This step happens automatically in the `npm run dev` script, but you could do it manually.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`app/index.tsx` in `@app/expo` workspace
+
+```typescript filename="apps / expo / app / index.tsx"
+import HomeRoute from '@app/routes/index'
+
+export default HomeRoute
+```
+
+`app/page.tsx` in `@app/next` workspace
+
+```typescript filename="apps / next / app / page.tsx"
+import HomeRoute from '@app/routes/index'
+
+export default HomeRoute
+
+// Re-export the route segment configs here as well
+```
+
+> Check [Universal Routing](/universal-routing) docs later for a deeper understanding of how this all works under the hood.
+
+---
+
+## Powerful Results 💪
+
+Following these instructions has provided us with a **bunch of value** in **little time**:
+
+- **Hybrid UI** component that is **styled with tailwind**, but **actually native on iOS and Android**
+- Hybrid UI component that is **optimized for SEO, media queries and Web-Vitals** on Web
+- **Universal data-fetching** logic that **works on server, browser and mobile**
+---
+- 🤝 A **single source of truth** for all our props, args, responses, types, defaults and validation
+---
+- A **Back-end resolver** function we can call from other data resolvers or API routes
+- A **GraphQL API** powered by Apollo-Server, with **automatically inferred type definitions**
+- A **Next.js powered API** that we could expose to **third parties to integrate** with us
+
+---
+
+### Next steps and plugins 🚀
+
+Now that you know how to build write-once cross-platform apps, why not dive into the [Core Concepts](/core-concepts) section next? It will give you a deeper understanding of how to get the most out of this starterkit.
+
+
+
+__In the near future__ (🔵/⏳), you can expand the core setup with **ready to merge `git based plugins`**, so you can **pick and choose the rest of your stack**:
+
+- 🔵 Database: `Supabase` / `Prisma` / `Drizzle` / `✅ Mongoose` / ...?
+- 🔵 Authentication: `Clerk` / `Kinde` / `Supabase` / custom?
+- 🔵 Payments: `Stripe` / `Lemonsqueezy` / other?
+- 🔵 Email: `react-email` + `Resend` / `Mailgun` / ...?
+- 🔵 Storage: `UploadThing` / `Supabase` / ...?
+- 🔵 UI kit: `Tamagui` / `Gluestack` / ...?
+
+If none of these options work for you, feel free to add what you're familiar with.
+
+Our core is the GREEN stack and we make absolutely no assumptions about the rest of your stack.
+
+> We will provide plugins with [zod based drivers](TODO) for the most popular options (listed above). Drivers and plugins are entirely optional and can be completely ignored if you don't need them.
+
+So, merge what you're familiar with, or check out the individual PR's to test and learn how they differ before making a decision.
+
+### Automatic Docgen, maybe?
+
+
+
+One plugin we recommend to everyone, is the `with/automagic-docs` plugin branch. It will enable pairing your [zod schemas](/single-sources-of-truth) / [Single sources of Truth](/single-sources-of-truth) with components, resolvers and API routes to automatically generate interactive docs for them. (Like Storybook, but in Next.js)
+
+This way:
+- Your docs grow with your project. 🚀
+- You'll easily onboard new people so they don't reinvent the wheel. 🤝
+- You ease technical handovers in case of acquisition or passing on the project.
+
+Just like the other [Core concepts](/core-concepts) and plugins, the documentation plugin is also Designed for Copy-paste.
+
+> Check the pages under "Application Features" in the sidebar for some examples of this plugin in action.
+
+We wish you the best on your _Full-Product, Universal App_ journey! 🎉
diff --git a/apps/docs/pages/project-structure.mdx b/apps/docs/pages/project-structure.mdx
new file mode 100644
index 0000000..fcc4442
--- /dev/null
+++ b/apps/docs/pages/project-structure.mdx
@@ -0,0 +1,414 @@
+import { Image } from '@app/primitives'
+import { FileTree } from 'nextra/components'
+
+
+
+# Project structure
+
+```shell
+your-universal-app/
+
+ └── /apps/
+ └── /expo/... # Expo workspace (iOS + Android)
+ └── /next/... # Next.js workspace (Web, SSR, API)
+
+ └── /features/... # Portable feature workspaces
+
+ └── /@app-core/...
+ └── /some-other-feature/...
+
+ └── /packages/... # Portable package workspaces
+
+ └── /@green-stack-core/...
+ └── /some-utility-package/...
+```
+
+## Why not just a single workspace?
+
+There's two main reasons why the starterkit is structured this way:
+
+1. Historically, using a monorepo for combining Expo & Next.js was better as you could **keep their configs and dependencies separate**. Meaning you can configure and upgrade Expo without upgrading Next.js and vice versa. *This still holds true for the most part.*
+
+2. It facilitates **architecting for copy-paste**. A workspace folder within a monorepo is the ideal unit of work to copy and paste between projects. You define your custom code and the dependencies it needs in a single folder, and can then consume it in another workspace. Colocating the UI / API / models / schemas / utils / constants / resolvers / components / hooks for a single, portable, feature.
+
+
+
+## Monorepo workspaces
+
+
+
+
+ Note on terminology
+
+
+> We tend to mean **"[package workspace](https://vercel.com/docs/vercel-platform/glossary#package)"** when we mention 'workspace', as defined by Vercel in their glossary:
+
+> "A **collection of files and directories** that are **grouped together based on a common purpose**. Types of packages include libraries, applications, services, and development tools."
+
+> "This **modular approach** is essential to monorepos, a repository structure that houses multiple interconnected packages, facilitating streamlined management and development of large-scale projects."
+
+> "In JavaScript, **each 'package workspace' has a `package.json` file at its root**, which contains metadata about the package, including its name, version, and any dependencies."
+
+### Feature workspaces
+
+You'll likely write most of your app code in feature workspaces. These are subfolders of the `/features/` folder.
+
+The main one is called `@app/core`. You could write your entire app in this workspace, but that would defeat the purpose of making your app features modular and portable.
+
+Instead, consider units of work that you'll likely need in other projects. Some good examples may include:
+
+```shell
+your-universal-app/
+
+ └── /features/...
+
+ └── /@app-core/...
+
+ └── /admin-panel/...
+
+ └── /blog-engine/...
+
+ └── /forum/...
+
+ └── /roadmap/...
+```
+
+Each of these might have specific routes or route-segments, data shapes, components and business logic that's mostly the same across projects.
+
+Think of these as the main sections of your app that would feel like either bloating the `@app/core` workspace, or that you know you'll reuse later in another project.
+
+### Package workspaces
+
+Package workspaces are less defined by features and more focused on utility. They're the place to put code that you'd like to consume across feature workspaces. Kind of like an NPM package, but local, editable and portable.
+
+A common usecase will likely be driver-like behaviour, e.g.
+
+```shell
+your-universal-app/
+
+ └── /packages/...
+
+ └── /@db-driver/... # <- e.g. main db driver, import as '@db/driver'
+
+ └── /@db-mongoose/... # <- '@db/mongoose' as a driver option
+
+ └── /@storage-driver/... # <- '@storage/driver'
+
+ └── /@storage-uploadthing/... # <- '@storage/uploadthing'
+
+ └── /@payments-stripe/...
+
+ └── /emails/...
+```
+
+### Workspace structure
+
+We recommend sticking to a similar structure for each workspace:
+
+```shell
+features/some-feature
+
+ └── /schemas/... # <- Single sources of truth
+ └── /models/... # <- Models / collections based on schemas
+ └── /resolvers/... # <- Reuses models & schemas
+
+ └── /components/...
+ └── /screens/... # <- Reuses components
+ └── /routes/... # <- Reuses screens
+ └── /api/... # <- Reuses resolvers
+
+ └── /assets/...
+ └── /icons/... # <- e.g. svg components
+ └── /constants/...
+ └── /utils/...
+
+ └── package.json # <- Name the workspace, manage dependencies
+```
+
+Each folder that follows this structure should have its own `package.json` file to define the package name and dependencies. This way, you can copy-paste a feature or domain from one project to another, and your JS package manager + bundler + runtime will have it work out of the box.
+
+Here's what this might look like in the full project.
+Don't hesitate to open the `/apps/`, `/features/` or `/packages/` folders:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+There are two main reasons for this recommended structure:
+
+1. ✅ **Group server and client code together** in the same workspace __so there's no big split between front and back-end__ folders. If we did do a front-end back-end split, you'd have to copy-paste multiple files to different destinations, as well as manage feature dependencies in two different places.
+
+2. ✅ It's **predictable**. Anything that's predictable can be used in automation, scripts and generators, which can further speed up your development in the long run.
+
+Aside from `/schemas/`, `/models/`, `/resolvers/`, `/routes/` and `/icons/`, we have no automations related to the other folders. The names are just suggestions, and you can rename, add or split as you see fit.
+
+## Turborepo Basics
+
+The tool we use to manage the monorepo is called **[Turborepo](https://turbo.build/repo)**.
+
+It's a wrapper around package manager (npm / yarn / pnpm) workspaces that adds some extra features.
+
+> Most importantly, **Turborepo goes out of its way to get out of your way**:
+
+- Does the least amount of work possible
+- Tries to never redo work that's already been done before
+- Caches tasks and results to speed up subsequent runs on your local machine
+
+You can also [share your cache](https://turbo.build/repo/docs/getting-started/existing-monorepo#using-remote-caching-for-local-development) between teams and projects to speed up CI builds.
+
+### Minimum requirements
+
+- [Specifying packages in a monorepo](https://turbo.build/repo/docs/crafting-your-repository/structuring-a-repository#specifying-packages-in-a-monorepo)
+- [A package manager lockfile](https://turbo.build/repo/docs/crafting-your-repository/structuring-a-repository#package-manager-lockfile)
+- [Root `package.json`](https://turbo.build/repo/docs/crafting-your-repository/structuring-a-repository#root-turbojson)
+- [Root `turbo.json`](https://turbo.build/repo/docs/crafting-your-repository/structuring-a-repository#root-turbojson)
+- [`package.json` in each feature / package / app workspace](https://turbo.build/repo/docs/crafting-your-repository/structuring-a-repository#packagejson-in-each-package)
+
+> All of this is already set up in the starterkit. However, you might want to keep this in mind when creating new feature or package workspaces.
+
+### Configure tasks with `turbo.json`
+
+The root `turbo.json` file is where you'll register the tasks that Turborepo will run. Once you have your tasks defined, you'll be able to run one or more tasks, e.g.
+
+```
+turbo run lint test build workspace#specific-script-name
+```
+
+Which would run the lint, test and then build tasks in all workspaces, as well as run the "`specific-script-name`" task in the "`workspace`" workspace. All in parallel, unless we define otherwise:
+
+https://turbo.build/repo/docs/crafting-your-repository/configuring-tasks
+
+> Have a look at the `turbo.json` in the root of the starterkit to see how we've set up the tasks for this project.
+
+### Speed up development with Turborepo generators
+
+
+
+Turborepo comes out of the box with a [code generator system](https://turbo.build/repo/docs/guides/generating-code).
+
+For exmaple, to add a new workspace interactively, we've added a generator:
+
+```shell
+npx turbo gen add-workspace
+# or `npm run gen add-workspace`
+```
+
+`⬇⬇⬇`
+
+```shell
+>>> Modify "project name" using custom generators?
+
+? what type of workspace would you like to generate?
+
+❯ feature
+ package
+```
+
+Depending on which other plugin branches you've merged, there may be other generators available to you.
+
+To check which generators are available:
+
+```shell
+npx turbo gen # interactive list to pick from
+```
+
+```shell
+>>> Modify "project name" using custom generators
+
+? Select generator to run (Use arrow keys)
+
+❯ add-dependencies: Install Expo SDK compatible deps
+ add-workspace: Create new feature or package workspace
+```
+
+> More about FullProduct.dev specific generators on our [generators page](/generators).
+
+## The registry pattern
+
+Ofcourse, any portable code you write in an isolated workspace is useless if you ultimately can't tie it all together in your apps or core features.
+
+That's where the registry pattern comes in:
+
+1. Collect different exports from separate workspaces
+2. Re-export the most important parts in a single place.
+
+This is done in the `@app/registries` workspace:
+
+```shell
+your-universal-app/
+
+ └── /packages/...
+
+ └── /@green-stack-core/...
+
+ └── /scripts/... # <- Scripts to collect files from workspaces
+
+ └── /@registries/... # ⬇⬇⬇ Collection of generated files from other workspaces
+
+ # Drivers - e.g. result of `npm run collect:drivers`
+ └── drivers.config.ts # driver enums & types
+ └── drivers.generated.ts # barrel of drivers
+
+ # Barrel file of DB models - `collect:models`
+ └── models.generated.ts
+
+ # Turborepo generators - `collect:generators`
+ └── generators.generated.ts
+
+ # GraphQL resolvers - `collect:resolvers`
+ └── resolvers.generated.ts
+
+ # Next.js route list - `link:routes`
+ └── routeManifest.generated.ts # types for Link component
+
+ # Workspace helpers - `check:workspaces`
+ └── workspaceResolutions.generated.js
+ └── transpiledWorkspaces.generated.js # used in next.config
+```
+
+> The `npm run dev` script will run all the necessary scripts to collect to rebuild these files automatically.
+
+### The glue when designing features for copy-paste
+
+This pattern is what facilitates the copy-paste design of workspaces in this starterkit. It allows you to build a feature in isolation, defining its own routes, UI and logic. Then you use the registry to plug it into your app to make it work, *and* be typesafe.
+
+For example, how our `Link` component knows about all the routes in the app:
+
+```shell
+your-universal-app/
+
+ └── /packages/...
+
+ └── /@registries/...
+
+ # Types for Link component
+ └── routeManifest.generated.ts # list of possible routes
+
+ └── /@green-stack-core/... # ⬇⬇⬇
+
+ └── /navigation/...
+ └── /Link.ts # <- Used here to provide editor hints
+```
+
+In the other docs, we'll dive deeper into what each of these registries is used for.
+
+## Further reading
+
+- [Monorepo.tools](https://monorepo.tools)
+- [NPM workspaces docs](https://docs.npmjs.com/cli/v7/using-npm/workspaces)
+- [Turborepo docs](https://turbo.build/repo/docs)
diff --git a/apps/docs/pages/single-sources-of-truth.mdx b/apps/docs/pages/single-sources-of-truth.mdx
new file mode 100644
index 0000000..c8a0cc4
--- /dev/null
+++ b/apps/docs/pages/single-sources-of-truth.mdx
@@ -0,0 +1,557 @@
+import { Image } from '@app/primitives'
+import { FileTree } from 'nextra/components'
+import { TitleWrapper, Hidden } from '@app/docs/components/Hidden'
+
+
+ ## Intro - Schemas for Abstractions
+
+
+
+
+# Schemas as Single Sources of Truth
+
+```ts copy
+import { z, schema } from '@green-stack/schemas'
+```
+
+In the [Project Structure](/project-structure) docs, we talked about how predictable patterns in folders and files can help you move faster through automation.
+
+This guide explains how **[Zod](https://zod.dev) based schemas** can help you gain even more speed through a **predictable** way of **defining your data shapes**, *when paired with tooling built around it*:
+
+A core feature of this universal starterkit is taking what works and making it better. This is why we invented `schema()` as a **tiny wrapper around zod's `z.object()`**. You can use it to define your datastructures *just once* for the entire monorepo.
+
+> `zod` is a schema validation library built with Typescript in mind. By extending it with `schema()`, we can leverage its powerful features to create single sources of truth for GraphQL, API handlers, Database Models and even automatic component docs.
+
+## Why Single Sources of Truth?
+
+
+
+### The problem
+
+Think about all the places you might need to redefine the structure of your data. Quite an extensive list for what is **essentially describing the same data**.
+
+
+
+Generally speaking, you never want to define your data shape more than once. Not only is it redundant and a pain to do, it's also a recipe for disaster.
+
+If you need to change something, you have to remember to change it in all the places. If at any point you forget to do that, then you risk your datastructures getting out of sync. When that happens, it will likely lead to outdated editor hints or docs at best, and bugs or even crashes at worst.
+
+
+
+### The solution
+
+Now Imagine you can get all of this from just one schema:
+
+- ✅ Types
+- ✅ Validation + defaults
+- ✅ DB models
+- ✅ API inputs & outputs
+- ✅ Form state
+- ✅ Documentation
+- ✅ Mock & test data
+- ✅ GraphQL schema defs
+
+You can use `schema()` to build out the shape of our data in one go. The resulting object will enable us to create all other definitions from it for (e.g.) GraphQL, DB models, docs and more. Meaning we can avoid ever declaring it again.
+
+This is a huge win for maintainability and developer experience, as it avoids the need to keep it all in sync. No more redeclaring the same data shape for all your component props, database models or function args / responses.
+
+## Building Schemas with Zod
+
+
+
+
+
+Let's have a look at how `zod` and `schema()` defs translate to Typescript types: 👇
+
+An object in this sense is a key-value pair, often used to represent the shape of a data "unit":
+
+```shell
+some-workspace
+
+ └── /schemas/... # <- Single sources of truth
+ └── User.ts # <- e.g. User schema
+```
+
+```tsx {4, 8} /Requires a name/1 /'User'/ filename="User.ts"
+export const User = schema('User', {
+ // Requires a name value (☝️) to port to other formats later, keep it the same as your schema and TS type
+
+ // Zod can help you go even narrower than typescript
+ name: z.string().min(2), // <- e.g. Needs to be a string with at least 2 letters
+ age: z.number().min(18), // <- e.g. Age must be a number of at least 18
+
+ // Just like TS, it can help you indicate fields as optional
+ isAdmin: z.boolean().default(false), // <- Marked optional, defaults to false
+ birthdate: z.Date().nullish(), // = same as calling .nullable().optional()
+})
+```
+
+> Check the full [schema reference docs](/@green-stack-core/schemas) for all available way to build and describe schemas.
+
+### Extracting Types
+
+The main thing to use schemas for is to hard-link validation with types.
+
+You can extract the type from the schema using `z.infer()`, `z.input()` or `z.output()`:
+
+```tsx filename="User.ts"
+// Extract type from the schema and export it as a type alias
+export type User = z.infer
+
+// If you have defaults, you can use z.input() or z.output() instead
+export type UserOutput = z.output
+export type UserInput = z.input
+```
+
+`⬇⬇⬇`
+
+```tsx /?/
+// {
+// name: string,
+// age: number,
+// isAdmin?: boolean,
+// birthDate?: Date | null,
+// }
+```
+
+> In this case where we check the resulting type of `z.input()`, the 'isAdmin' field will be marked as optional, as it's supposedly not defaulted to `false` yet. If we'd inspect `z.output()`, it would be marked as required since it's either provided or presumed defaulted.
+
+### Schema Validation
+
+You can use the `.parse()` method to validate inputs against the schema:
+
+```tsx {1, 4} /.shape.age/
+// Call .parse() on the whole User schema...
+const newUser = User.parse(someInput) // <- Auto infers 'User' type if valid
+
+// ...or validate idividual fields by using '.shape' 👇
+User.shape.age.parse("Invalid - Not a number")
+// Throws => ZodError: "Expected a number, recieved a string."
+// Luckily, TS will already catch this in your editor ( instant feedback 🙌 )
+```
+
+If a field's value does not match the schema, it will throw a `ZodError`:
+
+```ts {8, 14} /.issues/
+try {
+
+ // 🚧 Will fail validation
+ const someNumber = z.number().parse("Not a number")
+
+} catch (error) { // ⬇⬇⬇
+
+ /* Throws 'ZodError' with a .issues array:
+ [{
+ code: 'invalid_type',
+ expected: 'number',
+ received: 'string',
+ path: [],
+ message: 'Expected number, received string',
+ }]
+ */
+
+}
+```
+
+## Reusing and Expanding schemas
+
+It can happen that you need to differentiate between two similar data shapes, for example, needing to expand on an existing shape.
+
+
+
+
+
+
+
+
+You can add new fields by calling `.extendSchema()` on the original schema:
+
+```ts {6} /.extendSchema/ filename="AdminUser.ts"
+// Extend the User schema
+const AdminUser = User.extendSchema('AdminUser', {
+ isAdmin: z.boolean().default(true),
+})
+
+type AdminUser = z.infer
+
+// {
+// name: string,
+// age: number,
+// birthDate?: Date | null,
+//
+// isAdmin?: boolean, // <- New field added
+// }
+```
+
+> You will **need to provide a new name** for the extended schema. This ensures there is no conflict with the original one when we port it to other formats.
+
+There are other ways to create schemas from other ones, similar to how you would do it with Typescript:
+
+- [`.pickSchema()`](/@green-stack-core/schemas#pickschema---select-fields) - to pick a subset of fields
+- [`.omitSchema()`](/@green-stack-core/schemas#omitschema---remove-fields) - to remove a subset of fields
+
+### Defining Defaults
+
+You can mark fields as optional or provide default values by using either:
+- `.optional()` - to allow `undefined`
+- `.nullable()` - to allow `null`
+- `.nullish()` - to allow both
+
+You can also use `.default()` to provide a default value when the field isn't passed in:
+
+```ts {8} /.optional()/ /.nullable()/ /.nullish()/ /.default/2 filename="User.ts"
+// Define a schema with optional and nullable fields
+const User = schema('...', {
+
+ name: z.string().optional(), // <- Allow undefined
+ age: z.number().nullable(), // <- Allow null
+ birthData: z.date().nullish(), // <- Allow both
+
+ // Use .default() to make optional in args,
+ // but provide a default value when it IS undefined
+ isAdmin: z.boolean().default(false), // <- false
+})
+```
+
+> When using `.default()`, you might need to be more specifc when inferring types. You can use `z.input()` or `z.output()` to get the correct type. Based on which you choose, defaulted fields will be either optional or required.
+
+### Marking fields as sensitive
+
+```ts
+ password: z.string().sensitive()
+```
+
+In your schemas, you can mark fields as sensitive using `.sensitive()`. This will:
+
+- Exclude the field from appearing in the GraphQL schema, introspection or queries
+- Mark the field as strippable in API resolvers / responses (*)
+- Mark the field with `isSensitive: true` in schema introspection
+
+## Transforming to other formats
+
+With both types and validation in place, you can now transform your schemas to other formats. This is where the real power of `schema()` comes into play.
+
+### Schema introspection
+
+Before you can transform your schemas, you need to introspect them. This is done by calling `.introspect()` on the schema:
+
+```ts {3, 11} /User.introspect()/
+const userShapeMetadata = User.introspect()
+
+// ⬇⬇⬇ Lists the JSON representation of your data shape
+
+{
+ "name": "User",
+ "zodType": "ZodObject", // <- Zod class it was built with, e.g. z.object()
+ "baseType": "Object", // <- e.g. "Array" / "Boolean" / "String" / ...
+ "schema": {
+
+ // ☝️ Nested properties will be listed under "schema", e.g. object fields
+ "name": {
+ "zodType": "ZodString",
+ "baseType": "String",
+ "isOptional": false, // in case of .optional() or .default()
+ },
+ "age": {
+ zodType: "ZodNumber",
+ baseType: "Number", ...
+ },
+ "birthDate": {
+ zodType: "ZodDate",
+ baseType: "Date", ...
+ }
+
+ }
+}
+```
+
+> Don't worry though, you're unlikely to have to do this manually in your day-to-day work.
+
+A **deep introspection API** is what allows the kit to transform these zod schemas to other formats. Introspection is what enables schemas to serve as the **Single Sources of Truth for all data shapes**, *making them quite portable*.
+
+### Introspection metadata
+
+This is all possible metadata you can extract with `.introspect()` **from a single schema field**:
+
+```ts
+type Metadata = {
+
+ // Essentials
+ name?: string, // <- The name you passed to schema(), e.g. 'User'
+ zodType: ZOD_TYPE,
+ baseType: BASE_TYPE,
+
+ // Optionality and defaults
+ isOptional?: boolean,
+ isNullable?: boolean,
+ defaultValue?: any$Unknown,
+
+ // Documentation
+ exampleValue?: any$Unknown,
+ description?: string,
+ minLength?: number,
+ maxLength?: number,
+ exactLength?: number,
+ minValue?: number,
+ maxValue?: number,
+
+ // Flags
+ isInt?: boolean,
+ isBase64?: boolean,
+ isEmail?: boolean,
+ isURL?: boolean,
+ isUUID?: boolean,
+ isDate?: boolean,
+ isDatetime?: boolean,
+ isTime?: boolean,
+ isIP?: boolean,
+
+ // Literals, e.g. z.literal()
+ literalValue?: any$Unknown,
+ literalType?: 'string' | 'boolean' | 'number',
+ literalBase?: BASE_TYPE,
+
+ // e.g. Nested schema field(s) to represent:
+ // - object properties (like meta for 'age' / 'isAdmin' / ...)
+ // - array elements
+ // - tuple elements
+ schema?: S,
+
+ // The actual Zod object, only included with .introspect(true)
+ zodStruct?: z.ZodType & { ... }, // <- Outer zod schema (e.g. ZodDefault)
+ innerStruct?: z.ZodType & { ... }, // <- Inner zod schema (not wrapped)
+
+ // Mark as serverside only, strippable in API responses
+ isSensitive?: boolean,
+
+ // Compatibility with other systems like databases & drivers
+ isID?: boolean,
+ isIndex?: boolean,
+ isUnique?: boolean,
+ isSparse?: boolean,
+}
+```
+
+This is just to show how it works under the hood. You're unlikely to need to use this directly in your day-to-day work.
+
+> Are you building your own tools that hook into the introspection result?
+
+> Then you can use these generic metadata types to help provide type-safety:
+
+```ts copy
+import type { Metadata, Meta$Schema, Meta$Tuple, ... } from '@green-stack/schemas'
+```
+
+A good example of how introspected schemas can be transformed through is in the [Automatic MDX Docs](TODO) plugin:
+
+### Automatic MDX docgen
+
+```shell
+git merge with/automatic-docs
+```
+
+There's two ways schemas are used to create docs:
+
+1. **GraphQL schema comments** provide hints in the GraphQL playground - at `/api/graphql`
+2. **MDX component docs** generated from the prop schema, with interactive controls
+
+> No. 1 already works without any plugin when using our [recommended way to do GraphQL](/data-resolvers)
+
+> No. 2 happens automatically when running `npm run dev` after merging the docs plugin. ([see demo](/@app-core/components/Button))
+
+**For the best documentation experience, you'll want to add some important metadata:**
+
+- `.describe()` - to add a description to the field
+- `.example()` - to provide an example value for previews / prop tables
+- `.default()` - to provide a default value (also counts as example value)
+
+Let's say you have a component with props defined in a schema, like a `` component:
+
+```tsx {3, 6, 9, 15} filename="UserProfile.tsx"
+const UserProfileProps = schema('UserProfileProps', {
+
+ // Add a description to the field
+ name: z.string().describe('The name of the user'),
+
+ // Provide an example value
+ age: z.number().example(25),
+
+ // Add a description and example value
+ birthDate: z
+ .date()
+ .describe('User birthdate')
+ .example('1996-12-19'),
+
+ // Default values can be used as examples as well
+ isAdmin: z.boolean().default(false),
+})
+
+// Type alias
+type UserProfileProps = z.infer
+
+/* --- --- */
+
+export const UserProfile = (props: UserProfileProps) => {
+ // ...
+}
+```
+
+Export the prop introspection result from the same file. This links the props schema to the component so it gets picked up by the docs generator:
+
+```shell {4} /getDocumentationProps/
+some-workspace
+ └── /components/...
+ └── UserProfile.tsx # <- export `getDocumentationProps`
+```
+
+You can use `.documentationProps()` on the props schema. This will match export the introspection so it matches the expected format and provides hints to further refine your docs. It will call `.introspect()` internally to include the metadata we need to render in the preview + prop table:
+
+```tsx {5} filename="UserProfile.tsx"
+// ...
+
+/* --- Docs --- */
+
+export const getDocumentationProps = UserProfileProps.documentationProps('UserProfile', {
+
+ // 🚧 All these options are optional:
+
+ valueProp: '...', // <- For form components, e.g. 'defaultValue', saved to docs URL state
+ onChangeProp: '...', // <- For form components, e.g. 'onTextChange', triggers save to URL state
+
+ // Alternative way to provide example props, e.g.:
+ exampleProps: {
+ name: 'John Doe',
+ age: 25,
+ birthDate: '1996-12-19',
+ isAdmin: false,
+ },
+
+})
+```
+
+Think of `getDocumentationProps` as a way to mark the component as documentable. It's a convention that the docs plugin will look for when generating the MDX docs.
+
+The end result will look like a Storybook-like documentation page for your component, with:
+
+- ✅ Component name & import path
+- ✅ Live preview of the component
+- ✅ Props table with types, descriptions, examples, defaults
+- ✅ Copyable component code for current prop settings
+- ✅ Interactive controls to update both live preview, code example and URL
+
+Check out a live example for the interactive [Button](/@app-core/components/Button) component docs:
+
+[](/@app-core/components/Button)
+
+### Custom Zod schema integrations
+
+**Out of the box:**
+
+- `npm run build:schema` - Builds your `schema.graphql` from zod schemas
+- `bridgedFetcher()` - Data fetcher that auto-scaffolds GraphQL query from zod input + output schemas
+- [`useFormState()`](/form-management) - Form state hook to provide typed form state and validation utils
+- `createSchemaModel()` - Creates `mock` model until merging any DB driver plugin 👇
+
+**In the near future, we will add plugin branches that further integrate zod schemas with DB and API solutions**
+
+
+
+**Interactive DB driver plugins:**
+
+- `with/mongoose` - zod to mongoose
+- `with/supabase` - zod to supabase
+- `with/prisma` - zod to prisma
+- `with/drizzle` - zod to drizzle
+- `with/airtable` - zod to airtable
+
+**API plugins:**
+
+- `with/trpc` - Pair zod Data Bridges with resolvers to create tRPC handlers
+
+> Note that these plugins are only available in the paid version of the starterkit.
+
+
+
+Using introspection and the resulting metadata, you could also create your own custom integrations.
+
+## Schema generator
+
+Like all elements of our recommended way of working, there is a turborepo generator to help create a schema in a specific workspace:
+
+```shell copy
+npm run add:schema
+```
+
+`⬇⬇⬇`
+
+will prompt you for a target workspace and name:
+
+```shell
+>>> Modify "your-project" using custom generators
+
+? Where would you like to add this schema?
+❯ @app/core # -- from features/app-core
+ some-feature # -- from features/some-feature
+ some-package # -- from packages/some-package
+```
+
+`⬇⬇⬇`
+
+```shell
+>>> Modify "your-project" using custom generators
+
+? Where would you like to add this schema? # @app/core
+? What is the schema name? # SomeData
+? Optional description? # ...
+
+>>> Changes made:
+ • /features/@app-core/schemas/SomeData.ts # (add)
+ • Opened 1 file in VSCode # (open-in-vscode)
+
+>>> Success!
+```
+
+`⬇⬇⬇`
+
+```shell
+@app-core
+
+ └── /schemas/...
+ └── SomeData.schema.ts
+```
+
+
+
+Though if you chose to also generate an integration, it might look like this instead:
+
+```shell
+@app-core
+
+ └── /schemas/...
+ └── SomeData.schema.ts
+
+ └── /hooks/...
+ └── useSomeData.ts # <- Form state hook using `useFormState()`
+
+ └── /models/...
+ └── SomeData.ts # <- `@db/driver` model using `createSchemaModel()`
+```
+
+
+
+> Pretty powerful, right?
+
+### Further reading
+
+From our own docs:
+- [Data Bridges for fetching](/data-resolvers#start-from-a-databridge) - Starterkit Docs
+
+Relevant external resources:
+- [Zod's official docs](https://zod.dev) - zod.dev
+- [The Joy of Single Sources of Truth](https://dev.to/codinsonn/the-joy-of-single-sources-of-truth-277o) - Blogpost by [Thorr ⚡️ @codinsonn.dev](https://codinsonn.dev)
diff --git a/apps/docs/pages/universal-routing.mdx b/apps/docs/pages/universal-routing.mdx
new file mode 100644
index 0000000..42e0344
--- /dev/null
+++ b/apps/docs/pages/universal-routing.mdx
@@ -0,0 +1,307 @@
+import { FileTree, Steps } from 'nextra/components'
+import { Image } from '@app/primitives'
+
+
+
+# Cross-Platform Navigation + Routing
+
+- [Making routes portable](/universal-routing#workspace-defined-routes)
+- [Adding page routes](/universal-routing#using-the-route-generator)
+- [``](/universal-routing#universal-link-component)
+- [`useRouter()`](/universal-routing#universal-userouter-hook)
+
+## Workspace Defined Routes
+
+> We strongly recommend you ***define routes on the workspace level.***
+
+This way you can:
+
+- ✅ **Colocate** routes with the rest of a feature's domain (*maximizes copy-paste-ability*)
+- ✅ **Reuse them across platforms** (*auto re-exported* to **`@app/expo`** and **`@app/next`**)
+
+```shell {4, 6}
+features/@some-feature
+
+ └── /components/...
+ └── /screens/... # <- Reuses components
+ └── /HomeScreen.tsx
+ └── /routes/... # <- Reuses screens
+ └── /index.ts # <- Reuses e.g. 'HomeScreen.tsx'
+```
+
+For an example of what this might look like in an actual project, check the example below. Don't hesitate to click open some of the folders to get a better idea of how the routes are structured:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+> The recommended and easiest way to create a new route in a workspace is to use the Route Generator:
+
+
+
+## Using the Route Generator
+
+```shell
+npm run add:route
+```
+
+> The turborepo route generator will ask you some questions, like which url you’d like the route to have, and will generate the empty screens and routes folders in the workspace of your choosing, e.g.:
+
+```shell {4}
+>>> Modify "your-project-name" using custom generators
+
+? Where would you like to add this new route? # (Use arrow keys)
+❯ features/@app-core -- importable from: '@app/core'
+ features/@some-feature -- importable from: '@app/some-feature'
+ features/@other-feature -- importable from: '@app/other-feature'
+```
+
+`⬇⬇⬇`
+
+```shell {8, 9, 10}
+>>> Modify "your-project-name" using custom generators
+
+? Where would you like to add this new route? # -> features/@app-core
+? What should the screen component be called? # -> NewRouteScreen
+? What url do you want this route on? # e.g. "/examples/[slug]"
+? Would you like to fetch initial data for this route from a resolver? # No data fetching
+
+>>> Changes made:
+ • /features/@app-core/screens/NewRouteScreen.tsx # (add)
+ • /features/@app-core/routes/examples/[slug]/index.tsx # (add)
+
+>>> Success!
+```
+
+> We'll be ignoring [providing initial data](/data-fetching) for now, but we'll come back to it in [Universal Data Fetching](/data-fetching).
+
+
+
+## Manually adding universal routes
+
+> You *could* define your routes twice, once in `@apps/expo` and once in `@apps/next`, in line with their way of working.
+
+> If you prefer to add routes manually, and like colocating to keep them portable, follow the steps below:
+
+
+
+### Add a screen component
+
+Add a new screen component in a workspace `/screens/` folder, e.g.:
+
+```tsx filename="NewRouteScreen.tsx"
+// ...
+
+/* --- -------------------- */
+
+const NewRouteScreen = (props: ...) => {
+
+ // ...
+}
+
+/* --- Exports ----------------------------- */
+
+export default NewRouteScreen
+
+```
+
+*We'll dive into how to best type the `props` for routes in the [Universal Data Fetching](/data-fetching) docs.*
+
+### Export screen in `/routes/`
+
+You can use Next.js style routing conventions to export the screen component in the same workspace's `/routes/` folder, e.g.:
+
+
+
+
+
+
+
+
+
+
+
+
+```tsx {2, 5} filename="examples/[slug]/index.tsx"
+
+export { default } from '@app/core/screens/NewRouteScreen'
+
+// -i- Export any other next.js routing config here
+export const dynamic = 'auto'
+export const dynamicParams = true
+export const revalidate = false
+export const fetchCache = 'auto'
+export const runtime = 'nodejs'
+export const preferredRegion = 'auto'
+export const maxDuration = 5
+
+```
+
+### Run `npm run link:routes`
+
+
+
+This command will automatically re-export all routes from the workspace to the platform-specific `/app/` folder for Expo and Next.js
+
+```shell
+npm run link:routes
+```
+
+> This deduplication will happen automatically when restarting with `npm run dev`
+
+`⬇⬇⬇`
+
+
+
+
+
+
+
+
+
+
+
+
+## Navigating pages
+
+You can navigate using the `useRouter` hook or `Link` component from `@green-stack/navigation`. They provide APIs familiar to how navigation works in both the Next.js and `expo-router` app routers.
+
+While they do use each platform's underlying navigation system, they are not limited to just `expo-router` or `next.js`. Instead they will provide universal navigation utility.
+
+
+
+### Universal `useRouter()` hook
+
+```tsx
+import { useRouter } from '@green-stack/navigation/useRouter'
+```
+
+You can use `router.push()` to navigate to a new page:
+
+```tsx
+const router = useRouter()
+
+router.push('/examples/[slug]', '/examples/123')
+```
+
+> `.push()` will use a push operation on mobile if possible.
+
+There are also other methods available on the `router` object:
+
+- `router.navigate()` - Navigate to the provided href
+- `router.replace()` - Navigate without appending to the history
+- `router.back()` - Go back in the history
+- `router.canGoBack()` - Check if there's history that supports invoking the `back()` function
+- `router.setParams()` - Update the current route query params without navigating
+
+
+
+### Universal `` component
+
+If you import the `Link` component from `@green-stack/navigation`, it will automatically use the correct navigation system for the platform you are on.
+
+```tsx copy
+import { Link } from '@green-stack/navigation'
+```
+
+However, you can also ***import it from `@app/primitives` to apply tailwind styles***:
+
+```tsx copy
+import { Link } from '@app/primitives'
+```
+
+You can use the `href` prop to navigate to a new page:
+
+```tsx {3}
+
+ See example
+
+```
+
+
+
+#### `UniversalLinkProps`
+
+| Property | Type | Description |
+|------------------------|--------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| children | React.ReactNode | The content to be rendered inside the link. |
+| href | string \| HREF | The path to route to on web or mobile. String only. Hints for internal routes provided through codegen. |
+| style | | Style prop: https://reactnative.dev/docs/text#style |
+| className | string | Nativewind classNames should be applied to either the parent or children of Link. Ideally, create or use a TextLink component instead. |
+| replace | boolean | Should replace the current route without adding to the history - Default: false. |
+| onPress | | Extra handler that fires when the link is pressed. |
+| target | | Specifies where to display the linked URL. |
+| asChild | boolean | **Mobile only** - Forward props to child component. Useful for custom buttons - Default: false. |
+| push | boolean | **Mobile only** - Should push the current route, always adding to the history - Default: true. |
+| testID | string \| undefined | **Mobile only** - Used to locate this view in end-to-end tests. |
+| nativeID | string \| undefined | **Mobile only** - Used to reference react managed views from native code. @deprecated use `id` instead. |
+| id | string \| undefined | **Mobile only** - Used to reference react managed views from native code. |
+| allowFontScaling | | **Mobile only** - Specifies whether fonts should scale to respect Text Size accessibility settings. |
+| numberOfLines | | **Mobile only** - Specifies the maximum number of lines to use for rendering text. |
+| maxFontSizeMultiplier | | **Mobile only** - Specifies the maximum scale factor for text. |
+| suppressHighlighting | | **Mobile only** - When true, no visual change is made when text is pressed down. |
+| scroll | boolean | **Web only** - Whether to override the default scroll behavior - Default: false. |
+| shallow | boolean | **Web only** - Update the path of the current page without rerunning getStaticProps, getServerSideProps or getInitialProps - Default: false. |
+| passHref | boolean | **Web only** - Forces `Link` to send the `href` property to its child - Default: false. |
+| prefetch | boolean | **Web only** - Prefetch the page in the background. Any `` that is in the viewport (initially or through scroll) will be preloaded. Prefetch can be disabled by passing `prefetch={false}`. When `prefetch` is set to `false`, prefetching will still occur on hover. Pages using [Static Generation](/docs/basic-features/data-fetching/get-static-props.md) will preload `JSON` files with the data for faster page transitions. Prefetching is only enabled in production. - Default: true |
+| locale | string \| false | **Web only** - The active locale is automatically prepended. `locale` allows for providing a different locale. When `false` `href` has to include the locale as the default behavior is disabled. |
+| as | Url \| undefined | **Web only** - Optional decorator for the path that will be shown in the browser URL bar. |
+
+
+
+## Further reading
+
+From our own docs:
+- [Building APIs](/data-resolvers)
+- [Universal Data Fetching](/data-fetching)
+
+Relevant external docs:
+- [Next.js Route segment config](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config) - Determine rendering & caching strategy per route
+- [Next.js Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers) - Used for [Data Resolvers and API's](/data-resolvers)
+- [Expo Router](https://docs.expo.dev/develop/file-based-routing/) and [React Navigation](https://reactnavigation.org/docs/getting-started) - Used under the hood on mobile
+
diff --git a/apps/docs/pages/write-once-styles.mdx b/apps/docs/pages/write-once-styles.mdx
new file mode 100644
index 0000000..5a99a5d
--- /dev/null
+++ b/apps/docs/pages/write-once-styles.mdx
@@ -0,0 +1,389 @@
+import { FileTree, Steps } from 'nextra/components'
+import { Image } from '@app/primitives'
+
+
+
+# Universal UI
+
+## Write and style once, render anywhere
+
+The default UI primitives to use for building Universal Apps are those that react-native comes with. Instead of using `
`, `
`, `` or ``, you instead use ``, `` and ``
+
+```typescript
+import { View, Text, Image } from 'react-native'
+// ☝️ Auto-transformed to 'react-native-web' in Next.js
+```
+
+You *can* use the `style` prop or react-native's `StyleSheet` API to style them.
+
+However, it's better for mobile-first to use tailwind-style `className` support through [Nativewind](https://www.nativewind.dev/):
+
+
+
+## Nativewind Basics
+
+
+
+NativeWind allows you to use Tailwind CSS to style your components in React Native. Styled components can be shared between all React Native platforms, using the best style engine for that platform; CSS StyleSheet on web and StyleSheet.create for native. It's goals are to provide a consistent styling experience across all platforms, improving Developer UX, component performance and code maintainability.
+
+On native platforms, NativeWind performs two functions. First, at build time, it compiles your Tailwind CSS styles into StyleSheet.create objects and determines the conditional logic of styles (e.g. hover, focus, active, etc). Second, it has an efficient runtime system that applies the styles to your components. This means you can use the full power of Tailwind CSS, including media queries, container queries, and custom values, while still having the performance of a native style system.
+
+On web, NativeWind is a small polyfill for adding className support to React Native Web.
+
+```ts {10, 19}
+import { View, Text, Image } from '@app/primitives'
+// ☝️ Import from 'nativewind' instead of 'react-native'
+
+
+
+// Use the 'className' prop like you would with tailwind on the web
+
+// ⬇⬇⬇
+
+// When rendering on iOS and Android:
+
+// 'px-2' -> { paddingLeft: 8, paddingRight: 8 }
+// 'max-w-[100px]' -> { maxWidth: 100 }
+// 'items-center' -> { alignItems: 'center' }
+// 'rounded-md' -> { borderRadius: 6 }
+
+// -- vs. --
+
+// When rendering on the server or browser:
+
+// 'px-2' -> padding-left: 8px; padding-right: 8px;
+// 'max-w-[100px]' -> max-width: 100px;
+// 'items-center' -> align-items: center;
+// 'rounded-md' -> border-radius: 6px;
+
+// (uses regular tailwind css stylesheet on web to apply as actual CSS)
+```
+
+
+
+### Responsive Design
+
+If you're doing SSR with responsive-design, this becomes real handy to apply media-queries:
+
+```ts {7, 13}
+
+
+// Will apply the classes from a mobile-first perspective:
+
+// ⬇⬇⬇
+
+// on screens smaller than the 'lg' breakpoint:
+
+// 'text-base' -> font-size: 16px;
+
+// -- vs. --
+
+// on screens larger than the 'lg' breakpoint:
+
+// 'lg:text-lg' -> @media (min-width: 1024px) {
+// .lg\:text-lg {
+// font-size: 18px;
+// }
+// }
+```
+
+> Check [nativewind.dev](https://nativewind.dev) and [tailwindcss.com](https://tailwindcss.com/) for a deeper understanding of [Universal Styling](/write-once-styles) and breakpoints
+
+
+
+## Create your own primitives
+
+
+
+### Platform Optimized Images
+
+Some primitives like the `Image` component have optimized versions for each environment:
+
+- `next/image` for web
+- `expo-image` for mobile
+
+To automatically use the right one per render context, we've provided our own universal `Image` component:
+
+```typescript
+import { Image } from '@green-stack/components/Image'
+```
+
+Which you might wish to wrap with Nativewind to provide class names to:
+
+```tsx {5} /styled/2 filename="styled.tsx"
+import { Image as UniversalImage } from '@green-stack/components/Image'
+// ☝️ Import the universal Image component
+import { styled } from 'nativewind'
+
+// ⬇⬇⬇
+
+export const Image = styled(UniversalImage, '')
+// ☝️ Adds the ability to assign tailwind classes
+```
+
+
+
+### `styled()`
+
+It's advised to create fixed styles for e.g. headings using this same method.
+
+
+
+
+
+
+
+
+
+
+
+Pre-styling can be done using the `styled()` util:
+
+```tsx {2} filename="styled.tsx"
+import { Text as RNText } from 'react-native'
+import { styled } from 'nativewind'
+
+// ... other re-exported predefined styles ...
+
+/* --- Typography ------------ */
+
+export const P = styled(RNText, 'text-base')
+export const H1 = styled(RNText, 'font-bold text-2xl text-primary')
+export const H2 = styled(RNText, 'font-bold text-xl text-secondary')
+export const H3 = styled(RNText, 'font-bold text-lg text-gray-100')
+// ☝️ These styles will always be applied unless overridden by the className prop
+
+```
+
+For convenience, we've already set up an `@app/primitives` alias that points to this `styled.tsx` file for you.
+
+Styles you apply while using these components can overwrite the predefined styles if you want them to:
+
+```tsx
+import { Image, View, H1, P } from '@app/primitives'
+
+// ⬇⬇⬇
+
+
+
+
+
+
+ {/* ☝️ overrides color from predefined, but keeps the other classes */}
+
+
+```
+
+
+
+### Prefilling other style props
+
+Next to just prefilling `className`, you can also prefill other props by passing them as the third argument to `styled()`, e.g.:
+
+```tsx filename="Checkbox.restyled.tsx"
+const RestyledCheckbox = styled(Checkbox, 'bg-primary', {
+ labelClassName: 'text-secondary', // <- Prefill the 'labelClassName' prop
+ iconColor: '#FFFFFF', // <- Prefill the 'iconColor' prop
+})
+```
+
+
+
+## Combining classnames with `cn()`
+
+```tsx
+import { cn } from '@app/primitives'
+```
+
+If you want to combine multiple classes, you can use the `cn()` utility:
+
+```tsx
+
+```
+
+This will only apply the second class if `someBoolean` is `true`.
+
+> Underneath, `cn()` uses a combination of [`twMerge()`](https://www.npmjs.com/package/tailwind-merge) and [`cslx()`](https://www.npmjs.com/package/clsx) to merge the classes together.
+
+
+
+## Theme management
+
+You can manage colors, default text sizes, spacing and other theme-related properties in a single place:
+
+
+
+
+
+
+
+
+
+
+```js {5} /--primary/1 filename="tailwind.theme.js"
+const universalTheme = {
+ // -i- Extend default tailwind theme here
+ // -i- Reference this theme in the tailwind.config.js files in apps/expo, apps/next, features/app-core and other package or feature folders
+ extend: {
+ colors: {
+ 'background': 'hsl(var(--background))',
+ 'foreground': 'hsl(var(--foreground))',
+ 'primary': 'hsl(var(--primary))',
+ 'primary-foreground': 'hsl(var(--primary-foreground))',
+ 'secondary': 'hsl(var(--secondary))',
+ 'secondary-foreground': 'hsl(var(--secondary-foreground))',
+ 'link': 'hsl(var(--link))',
+ 'muted': 'hsl(var(--muted))',
+ 'warn': 'hsl(var(--warn))',
+ ...
+ },
+ borderColor: (theme) => ({
+ ...
+ }),
+ backgroundColor: (theme) => ({
+ ...
+ }),
+ textColor: (theme) => ({
+ ...
+ }),
+ borderWidth: {
+ hairline: hairlineWidth(),
+ },
+ keyframes: ...,
+ animation: ...,
+ }
+}
+```
+
+It's advised to reuse this `universalTheme` in all your `tailwind.config.js` files.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+### Colors and Dark Mode
+
+You main colors can be managed in `global.css`.
+
+This is where you can define, for example, the colors for light and dark mode:
+
+```css {22, 25} /--primary/1 filename="global.css"
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/* --- Tailwind Theme -------------------------------------------------------------------------- */
+
+@layer base {
+ :root {
+ --background: 0, 0%, 100%; /* #FFFFFF; /* tailwind: colors.white */
+ --foreground: 240, 5%, 10%; /* #18181b; /* tailwind: colors.zinc[900] */
+ --primary: 222, 15%, 13%; /* #111827; /* tailwind: colors.gray[900] */
+ --primary-foreground: 0, 0%, 100%; /* #FFFFFF; /* tailwind: colors.white */
+ --secondary: 217, 13%, 19%; /* #1f2937; /* tailwind: colors.gray[800] */
+ --secondary-foreground: 210, 16%, 96%; /* #f3f4f6; /* tailwind: colors.gray[100] */
+ --link: 213, 94%, 76%; /* #93c5fd; /* tailwind: colors.blue[300] */
+ --muted: 220, 8%, 65%; /* #9ca3af; /* tailwind: colors.gray[400] */
+ --warn: 24, 89%, 47%; /* #ea580c; /* tailwind: colors.orange[600] */
+ ...
+ }
+
+ .dark:root {
+ --background: 216, 34%, 17%; /* #1e293b; /* tailwind: colors.slate[800] */
+ --foreground: 210, 16%, 96%; /* #f3f4f6; /* tailwind: colors.gray[100] */
+ --primary: 210, 16%, 96%; /* #f3f4f6; /* tailwind: colors.gray[100] */
+ --primary-foreground: 222, 15%, 13%; /* #111827; /* tailwind: colors.gray[900] */
+ ...
+ }
+}
+```
+
+
+
+### Theme colors as values
+
+You can retrieve theme colors as values by passing their CSS variable name to one of our style utils:
+
+- `getThemeColor()` -- Statically retrieves the color value
+- `useThemeColor()` -- Dynamically retrieves the color value in a component
+
+```tsx
+import { getThemeColor } from '@app/primitives'
+
+const primaryColor = getThemeColor('--primary')
+const secondaryColor = getThemeColor('--secondary')
+```
+
+```tsx
+import { useThemeColor } from '@app/primitives'
+
+// Within a React component:
+
+const primaryColor = useThemeColor('--primary')
+const secondaryColor = useThemeColor('--secondary')
+```
+
+
+
+## Recommended tooling
+
+For hover previews of what tailwind classes do, or hints for which are available, we recommend you install the following VSCode plugins:
+
+- [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)
+- [Tailwind Fold](https://marketplace.visualstudio.com/items?itemName=stivo.tailwind-fold)
+
+We recommend you add the following settings to your VSCode settings.json:
+
+```json {13} filename="settings.json"
+// ...
+
+"tailwind-fold.autoFold": false,
+"tailwind-fold.unfoldIfLineSelected": true,
+"tailwindCSS.classAttributes": [
+ "class",
+ "className",
+ "tw",
+ "tailwind",
+ "style",
+],
+"tailwindCSS.experimental.classRegex": [
+ "cn\\([\\s\\S]*?['\"]([^'\"\\s]*?)['\"]" // matches simple usage within cn function
+ "ClassName.*?z\\.string\\(\\).*?\\.default\\('([^']*)'", // tailwind class property default in zod schemas
+ "ClassName.*?z\\.string\\(\\).*?\\.eg\\('([^']*)'", // tailwind class property example in zod schemas
+ "Classes.*?z\\.string\\(\\).*?\\.default\\('([^']*)'", // tailwind class property default in zod schemas
+ "Classes.*?z\\.string\\(\\).*?\\.eg\\('([^']*)'", // tailwind class property example in zod schemas
+],
+
+// ...
+```
+
+
+
+## Official docs
+
+- [Nativewind](https://www.nativewind.dev/)
+- [Tailwind CSS](https://tailwindcss.com/)
+
diff --git a/apps/docs/postcss.config.cjs b/apps/docs/postcss.config.cjs
new file mode 100644
index 0000000..fef1b22
--- /dev/null
+++ b/apps/docs/postcss.config.cjs
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/apps/docs/remote/pluginsConfig.ts b/apps/docs/remote/pluginsConfig.ts
new file mode 100644
index 0000000..a7ee37c
--- /dev/null
+++ b/apps/docs/remote/pluginsConfig.ts
@@ -0,0 +1,46 @@
+export const pluginsConfig = {
+ branches: [
+ 'nativewind',
+ 'portability-patterns',
+ 'mdx',
+ 'graphql-server-apollo',
+ 'react-query',
+ 'mdx-docs-nextra',
+ 'turborepo',
+ 'green-stack'
+ ],
+ nestedMetaData: {
+ 'nativewind': {
+ 'title': 'Nativewind',
+ 'route': '/plugins/nativewind'
+ },
+ 'portability-patterns': {
+ 'title': 'React Portability Patterns',
+ 'route': '/plugins/portability-patterns'
+ },
+ 'mdx': {
+ 'title': 'Universal MDX',
+ 'route': '/plugins/mdx'
+ },
+ 'graphql-server-apollo': {
+ 'title': 'GraphQL Server (Apollo)',
+ 'route': '/plugins/graphql-server-apollo'
+ },
+ 'react-query': {
+ 'title': 'React Query',
+ 'route': '/plugins/react-query'
+ },
+ 'mdx-docs-nextra': {
+ 'title': 'MDX Docs (Nextra)',
+ 'route': '/plugins/mdx-docs-nextra'
+ },
+ 'turborepo': {
+ 'title': 'Turborepo',
+ 'route': '/plugins/turborepo'
+ },
+ 'green-stack': {
+ 'title': 'GREEN stack',
+ 'route': '/plugins/green-stack'
+ }
+ }
+}
diff --git a/apps/docs/scripts/regenerate-docs.ts b/apps/docs/scripts/regenerate-docs.ts
new file mode 100644
index 0000000..41957af
--- /dev/null
+++ b/apps/docs/scripts/regenerate-docs.ts
@@ -0,0 +1,1130 @@
+import fs from 'fs'
+import { parseWorkspaces, getAvailableSchemas, getAvailableDataBridges, swapImportAlias, globRel, hasOptOutPatterns, lowercaseFirstChar, createDivider, uppercaseFirstChar, maybeImport } from '@green-stack/scripts/helpers/scriptUtils'
+import type { SchemaFileMeta, BridgeFileMeta } from '@green-stack/scripts/helpers/scriptUtils'
+import { Meta$Schema, renderSchemaToZodDefV3, ZodSchema } from '@green-stack/core/schemas'
+import { zodToTs, printNode } from 'zod-to-ts'
+import { gen, createBridgedFormHookContent } from '@green-stack/core/generators/add-resolver'
+import { setProperty } from 'dot-prop'
+
+/* --- Constants ------------------------------------------------------------------------------- */
+
+const { workspacePaths, PATH_PKGS } = parseWorkspaces()
+const availableSchemas = getAvailableSchemas()
+const availableDataBridges = getAvailableDataBridges()
+
+/* --- Types ----------------------------------------------------------------------------------- */
+
+type ComponentDocsData = {
+ rootPath: string,
+ componentName: string,
+ componentWorkspace: string,
+ workspaceFolder: string,
+ importPath: string,
+ componentFileName: string,
+ mdxFilePath: string,
+ mdxFileFolder: string,
+ documentationProps?: {
+ componentName: string
+ propSchema: ZodSchema
+ propMeta: Record
+ previewProps: Record
+ },
+ propsSchemaDef?: string,
+ propsTypeDef?: string,
+ customMdxDocs?: string,
+}
+
+type ComponentDocsTree = {
+ [componentName: string]: ComponentDocsData
+}
+
+type SchemaContext = Prettify
+
+type ResolverContext = Prettify
+
+type CustomDocsData = {
+ entityName: string
+ workspaceName: string
+ workspaceFolder: string
+ mdxFilePath: string
+ mdxFileFolder: string
+ mdxContent: string
+}
+
+type CustomDocsTree = {
+ [mdxFilePath: string]: CustomDocsData
+}
+
+/** --- renderFileTree() ----------------------------------------------------------------------- */
+/** -i- Renders the string representation of multiple paths as a Nextra FileTree */
+const renderFileTree = (paths: string[]) => {
+ // Build object representation of the file tree
+ const fileTreeObj = paths.reduce((acc, path, i) => {
+ const [fileName, ...reversedFolders] = path.split('/').reverse()
+ const folders = reversedFolders.reverse()
+ return setProperty(acc, `${folders.join('.')}.file-${i}`, fileName)
+ }, {} as Record)
+ // Stringify the file tree and split into lines
+ const fileTreeJSON = JSON.stringify(fileTreeObj, null, 4)
+ const fileTreeLines = fileTreeJSON.split('\n').map((line) => {
+ if (line === '{') return ''
+ if (line === '}') return ''
+ if (line.includes('": {')) {
+ const [indent] = line.split('"')
+ const folderName = line.split('"')[1]
+ return `${indent}`
+ }
+ if (line.endsWith('}')) return line.replace('}', '')
+ // Final line case for file names
+ const [indent, _, __, fileName] = line.split('"')
+ return `${indent}`
+ })
+ // Join the lines into a single string
+ return fileTreeLines.join('\n')
+}
+
+/** --- createComponentDocsContent() ----------------------------------------------------------- */
+/** -i- Creates the file contents for component / UI docs based on props schema and component metadata */
+const createComponentDocsContent = (ctx: ComponentDocsData) => [
+
+ `import { ${ctx.componentName}, getDocumentationProps } from '${ctx.importPath}'`,
+ `import { ComponentDocs } from '@app/core/mdx/ComponentDocs'`,
+ `import { TitleWrapper } from '@app/docs/components/Hidden'`,
+ `import { FileTree, Callout } from 'nextra/components'\n`,
+
+ ``,
+ ` ## ${ctx.componentName}`,
+ `\n`,
+
+ `# ${ctx.componentName}\n`,
+
+ `\`\`\`typescript copy`,
+ `import { ${ctx.componentName} } from '${ctx.importPath}'`,
+ `\`\`\`\n`,
+
+ ``,
+ ` ### Interactive Preview`,
+ `\n`,
+
+ ``,
+ ` ### Code Example`,
+ `\n`,
+
+ ``,
+ ` ## ${ctx.componentName} Props`,
+ `\n`,
+
+ `\n`,
+
+ ...(ctx.propsSchemaDef ? [
+
+ ``,
+ ` ### Props Schema`,
+ `\n`,
+
+ ``,
+ `Show Props Schema\n`,
+
+ `\`\`\`typescript copy`,
+ `${ctx.propsSchemaDef}`,
+ `\`\`\``,
+
+ `> 💡 Could be handy to copy-paste into an AI chat?`,
+
+ `\n`,
+
+ `\n`,
+
+ ] : []),
+
+ ...(ctx.propsTypeDef ? [
+
+ ``,
+ `### Props Type`,
+ `\n`,
+
+ ``,
+ `Show Props Types\n`,
+
+ `\`\`\`typescript copy`,
+ `${ctx.propsTypeDef}`,
+ `\`\`\``,
+
+ `> 💡 Could be handy to copy-paste into an AI chat?`,
+
+ `\n`,
+
+ `\n`,
+
+ ] : []),
+
+ ``,
+ `## Source Code`,
+ `\n`,
+
+ `### File Location\n`,
+
+ `You can find the source of the \`${ctx.componentName}\` component in the following location:\n`,
+
+ `${renderFileTree([ctx.rootPath])}\n`,
+
+ `\n`,
+
+ ...(!!ctx.customMdxDocs ? [
+
+ `## Developer Notes\n`,
+
+ ctx.customMdxDocs,
+
+ `\n`,
+
+ ] : []),
+
+ `## Other\n`,
+
+ `### Disclaimer - Automatic Docgen\n`,
+
+ ``,
+ [
+ `These dynamic component docs were auto-generated with \`npm run regenerate-docs\`. `,
+ `You can hook into automatic docgen by exporting \`getDocumentationProps\` from a component file. `,
+ `You'll want to provide example props from the ComponentProps zod schema, e.g:`
+ ].join(''),
+ `\n`,
+
+ `\`\`\`tsx /getDocumentationProps/ /documentationProps/ copy filename="${ctx.rootPath.split('/').pop()}"`,
+ `/* --- Docs ---------------------- */\n`,
+
+ `export const getDocumentationProps = ${ctx.componentName}Props.documentationProps('${ctx.componentName}')`,
+ `\`\`\`\n`,
+
+].join('\n')
+
+/** --- createSchemaDocs() --------------------------------------------------------------------- */
+/** -i- creates the Schema MDX docs for a given schema and it's file metadata */
+const createSchemaDocs = (ctx: SchemaContext) => [
+
+ `import { FileTree, Callout } from 'nextra/components'`,
+ `import { TitleWrapper } from '@app/docs/components/Hidden'`,
+ `import { View, Image } from '@app/primitives'\n`,
+
+ ``,
+ ` ## ${ctx.schemaName}`,
+ `\n`,
+
+ `# ${ctx.schemaName}\n`,
+
+ `\`\`\`typescript copy`,
+ `import { ${ctx.schemaName} } from '${ctx.workspaceName}/schemas/${ctx.schemaFileName}'`,
+ `\`\`\`\n`,
+
+ `### Location\n`,
+
+ `${renderFileTree([ctx.schemaPath])}\n`,
+
+ `### Zod Schema\n`,
+
+ `What the schema would look like when defined with \`z.object()\` in Zod V3:\n`,
+
+ `\`\`\`typescript copy`,
+
+ `${ctx.schemaZodObjectDef}`,
+
+ `\`\`\`\n`,
+
+ `> (💡 Could be handy to copy-paste this schema info into an AI chat assistant)`,
+
+ `\n`,
+
+ `### Type Definition\n`,
+
+ `You can extract the TypeScript type from the schema using \`z.input()\`, \`z.output()\` or \`z.infer()\` methods. e.g.:\n`,
+
+ `\`\`\`typescript copy`,
+ `type ${ctx.schemaName} = z.input`,
+ `\`\`\`\n`,
+
+ `What the resulting TypeScript type would look like:\n`,
+
+ `\`\`\`typescript copy`,
+
+ `${ctx.schemaTypeDef}`,
+
+ `\`\`\`\n`,
+
+ `> (💡 Could be handy to copy-paste this type info into an AI chat assistant)`,
+
+ `\n`,
+
+ `### Usage - Validation\n`,
+
+ `To validate data against this schema, you have a few options:\n`,
+
+ `\`\`\`typescript copy`,
+
+ `// Throws if invalid`,
+ `const ${lowercaseFirstChar(ctx.schemaName)} = ${ctx.schemaName}.parse(data)\n`,
+
+ `// Returns { success: boolean, data?: T, error?: ZodError }`,
+ `const ${lowercaseFirstChar(ctx.schemaName)} = ${ctx.schemaName}.safeParse(data)\n`,
+
+ `\`\`\`\n`,
+
+ `> This might be useful for parsing API input data or validating form data before submission.\n`,
+
+ `> You can also directly integrate this schema with form state managers like our own:\n`,
+
+ `\n`,
+
+ `### Usage - Form State\n`,
+
+ `\`\`\`typescript copy`,
+
+ `import { useFormState } from '@green-stack/forms/useFormState'\n`,
+
+ `const formState = useFormState(${ctx.schemaName}, {`,
+ ` initialValues: { /* ... */ }, // Provide initial values?`,
+ ` validateOnMount: true, // Validate on component mount?`,
+ `})\n`,
+
+ `\`\`\`\n`,
+
+ `Learn more about using schemas for form state in our [Form Management Docs](/form-management).\n`,
+
+ `\n`,
+
+ `### Usage - Component Props / Docs\n`,
+
+ `Another potential use case for the '${ctx.schemaName}' schema is to type component props, provide default values and generate documentation for that component:\n`,
+
+ `\`\`\`typescript copy`,
+
+ `export const ${ctx.schemaName}ComponentProps = ${ctx.schemaName}.extend({`,
+ ` // Add any additional props here`,
+ `})\n`,
+
+ `export type ${ctx.schemaName}ComponentProps = z.input\n`,
+
+ `/* --- <${ctx.schemaName}Component/> --------------- */\n`,
+
+ `export const ${ctx.schemaName}Component = (rawProps: ${ctx.schemaName}ComponentProps) => {\n`,
+
+ ` // Extract the props and apply defaults + infer resulting type`,
+ ` const props = ComponentProps.applyDefaults(rawProps)\n`,
+
+ ` // ... rest of the component logic ...\n`,
+
+ `}\n`,
+
+ `/* --- Documentation --------------- */\n`,
+
+ `export const documentationProps = ${ctx.schemaName}ComponentProps.documentationProps('${ctx.schemaName}Component')\n`,
+
+ `\`\`\`\n`,
+
+ `\n`,
+
+ ...(!!ctx.customMdxDocs ? [
+
+ `## Developer Notes\n`,
+
+ ctx.customMdxDocs,
+
+ `\n`,
+
+ ] : []),
+
+ `## Other\n`,
+
+ `### Disclaimer - Automatic Docgen\n`,
+
+ ``,
+ [
+ `These dynamic schema docs were auto-generated with \`npm run regenerate-docs\`. `,
+ `This happens automatically for schema files in any \`\\schemas\\\` folder. `,
+ `You can opt-out of this by adding \`// export const optOut = true\` somewhere in the file. `,
+ ].join(''),
+ `\n`,
+
+].filter(Boolean).join('\n')
+
+/** --- createResolverDocs() ------------------------------------------------------------------- */
+/** -i- creates the Resolver MDX docs for a given schema and it's file metadata */
+const createResolverDocs = (ctx: ResolverContext) => [
+
+ `import { FileTree, Callout } from 'nextra/components'`,
+ `import { TitleWrapper } from '@app/docs/components/Hidden'`,
+ `import { View, Image } from '@app/primitives'\n`,
+
+ ``,
+ ` ## \`${ctx.resolverName}\` - API`,
+ `\n`,
+
+ `# ${ctx.resolverName}() - Resolver\n`,
+
+ `${renderFileTree([
+ `${ctx.workspacePath}/resolvers/${ctx.resolverFileName}.ts`,
+ `${ctx.workspacePath}/resolvers/${ctx.fetcherFileName}.ts`,
+ `${ctx.workspacePath}/resolvers/${ctx.resolverName}.bridge.ts`,
+ ])}\n`,,
+
+ `\`${ctx.resolverName}()\` is a ${ctx.resolverType} resolver that allows you to ${ctx.operationType} data from:\n`,
+
+ `- [Async Functions](#server-usage) during other resolver logic / GraphQL / API calls server-side`,
+ ctx.isGraphQl && `- [GraphQL](#graphql-${ctx.resolverType}) - As a GraphQL ${ctx.resolverType}`,
+ ctx.hasApiRoute && `- [API route](#nextjs-api-route) (${ctx.routeMethods.join(' / ')})`,
+ `- [Clientside Hooks](#client-usage) for calling the API with \`react-query\` from Web / Mobile\n`,
+
+ `\n`,
+
+ // --- Config -----------------------------------------------------------------------
+
+ `## Resolver Config\n`,
+
+ `Input / output types, defaults, schemas and general config for the \`${ctx.resolverName}()\` resolver are defined in its DataBridge file. Importable from:\n`,
+
+ `\`\`\`typescript copy`,
+ `import { ${ctx.resolverName}Bridge } from '${ctx.workspaceName}/resolvers/${ctx.resolverName}.bridge'`,
+ `\`\`\`\n`,
+
+ `\n`,
+
+ // --- Input ------------------------------------------------------------------------
+
+ `### Input Shape\n`,
+
+ `You can find the schema used to validate the input arguments for the \`${ctx.resolverName}()\` resolver in the bridge config:\n`,
+
+ `\`\`\`typescript copy`,
+ `const ${ctx.inputSchemaName} = ${ctx.resolverName}Bridge.inputSchema`,
+ `\`\`\`\n`,
+
+ ``,
+ `Show Input Schema\n`,
+
+ `\`\`\`typescript copy`,
+ `${ctx.inputSchemaDef}`,
+ `\`\`\``,
+
+ `> 💡 Could be handy to copy-paste into an AI chat?`,
+
+ `\n`,
+
+ `\n`,
+
+ `If needed, you can extract the TypeScript type from the schema using \`z.input()\`, e.g.:\n`,
+
+ `\`\`\`typescript copy`,
+ `type ${ctx.inputSchemaName} = z.input`,
+ `\`\`\`\n\n`,
+
+ ``,
+ `Show Input Type\n`,
+
+ `\`\`\`typescript copy`,
+ `${ctx.inputSchemaType}`,
+ `\`\`\``,
+
+ `> 💡 Could be handy to copy-paste into an AI chat?`,
+
+ `\n`,
+
+ `\n`,
+
+ // --- Output -----------------------------------------------------------------------
+
+ `### Output Shape\n`,
+
+ `You can find the schema used to provide output defaults for the \`${ctx.resolverName}()\` resolver in the bridge config too:\n`,
+
+ `\`\`\`typescript copy`,
+ `const ${ctx.outputSchemaName} = ${ctx.resolverName}Bridge.outputSchema`,
+ `\`\`\`\n`,
+
+ ``,
+ `Show Output Schema\n`,
+
+ `\`\`\`typescript copy`,
+ `${ctx.outputSchemaDef}`,
+ `\`\`\``,
+
+ `> 💡 Could be handy to copy-paste into an AI chat?`,
+
+ `\n`,
+
+ `\n`,
+
+ `Here too, you can extract the TypeScript type from the schema using \`z.output()\`, e.g.:\n`,
+
+ `\`\`\`typescript copy`,
+ `type ${ctx.outputSchemaName} = z.output`,
+ `\`\`\`\n`,
+
+ ``,
+ `Show Output Type\n`,
+
+ `\`\`\`typescript copy`,
+ `${ctx.outputSchemaType}`,
+ `\`\`\``,
+
+ `> 💡 Could be handy to copy-paste into an AI chat?`,
+
+ `\n`,
+
+ `\n`,
+
+ // --- Async Function Usage ---------------------------------------------------------
+
+ `## Server Usage\n`,
+
+ `\n`,
+
+ `### \`${ctx.resolverName}()\` function\n`,
+
+ `\`\`\`typescript copy`,
+ `import { ${ctx.resolverName} } from '${ctx.workspaceName}/resolvers/${ctx.resolverFileName}'`,
+ `\`\`\`\n`,
+
+ `\`\`\`typescript copy`,
+ `// ... Later, in resolver or script logic ...`,
+ `const output = await ${ctx.resolverName}({ ...inputArgs })`,
+ `// ?^ ${ctx.outputSchemaName} `,
+ `\`\`\`\n`,
+
+ `Note that using resolvers like \`${ctx.resolverName}()\` as async functions is only available server-side, and might cause issues if imported into the client bundle. For client-side usage, use any of the other options below.\n`,
+
+ `\n`,
+
+ // --- GraphQL Fetcher Usage --------------------------------------------------------
+
+ ...(ctx.isGraphQl ? [
+
+ `## GraphQL ${ctx.ResolverType}\n`,
+
+ `\n`,
+
+ `### \`${ctx.fetcherName}()\`\n`,
+
+ `\`\`\`typescript copy`,
+ `import { ${ctx.fetcherName} } from '${ctx.workspaceName}/resolvers/${ctx.fetcherFileName}'`,
+ `\`\`\`\n`,
+
+ `\`${ctx.fetcherName}()\` is a universal GraphQL ${ctx.resolverType} fetcher function. `,
+ `It wil query the \`${ctx.resolverName}\` resolver for you as a GraphQL ${ctx.resolverType}:\n`,
+
+ `\`\`\`typescript copy`,
+ `const response = await ${ctx.fetcherName}({ ${ctx.resolverArgsName}: { ...inputArgs } })`,
+ `// ?^ { ${ctx.resolverName}: ${ctx.outputSchemaName} } `,
+ `\`\`\`\n`,
+
+ `Just make sure the \`${ctx.resolverArgsName}\` input matches the [\`${ctx.inputSchemaName}\`](#input-shape) schema.\n`,
+
+ `If you prefer, you can also use the following GraphQL snippet in your own GraphQL fetching logic:\n`,
+
+ `\n`,
+
+ `### GraphQL ${ctx.ResolverType} Snippet\n`,
+
+ `\`\`\`graphql copy`,
+ `${ctx.graphqlQueryDef}`,
+ `\`\`\`\n`,
+
+ `\n`,
+
+ `### Custom ${ctx.ResolverType}\n`,
+
+ `Using a custom query, you can omit certain fields you don't need and request only what's necessary.\n`,
+
+ `If you do, we suggest using \`graphqlQuery()\`, as it works seamlessly on the server, browser and mobile app:\n`,
+
+ `\`\`\`typescript copy`,
+ `import { graphql } from '@app/core/graphql/graphql'`,
+ `import { graphqlQuery } from '@app/core/graphql/graphqlQuery'\n`,
+
+ `const query = graphql(\``,
+ `${ctx.graphqlQueryDef.split('\n').splice(0, 2).map(line => ` ${line}`).join('\n')}`,
+ ` // -i- ... type hints for the fields you need ... -i-`,
+ ` }`,
+ ` }`,
+ `\`)\n`,
+
+ `const response = await graphqlQuery(query, { ${ctx.resolverArgsName}: { ...inputArgs } })`,
+ `// ?^ { ${ctx.resolverName}: ${ctx.outputSchemaName} } `,
+ `\`\`\`\n`,
+
+ `Just make sure the \`${ctx.resolverArgsName}\` input matches the [\`${ctx.inputSchemaName}\`](#input-shape) schema.\n`,
+
+ `\n`,
+
+ ] : []),
+
+ // --- React-Query Hook -------------------------------------------------------------
+
+ ...(ctx.hasApiRoute ? [
+
+ `## Next.js API Route\n`,
+
+ `\n`,
+
+ ...ctx.routeMethods.map((method: string) => [
+
+ `### \`${method}\` requests\n`,
+
+ `\`\`\`shell copy`,
+ `${method} ${ctx.apiPath}${method === 'GET' ? '?...' : ''}`,
+ `\`\`\`\n`,
+
+ ['GET'].includes(method) && `Provide query parameters as needed (e.g. \`?someArg=123\`).\n`,
+
+ ['POST', 'PUT'].includes(method) && `Provide the request body as JSON (e.g. \`{ "someArg": 123 }\`).\n`,
+
+ `Make sure the params / ${method === 'GET' ? 'query' : 'body'} input match the [\`${ctx.inputSchemaName}\`](#input-shape) schema.\n`,
+
+ `\n`,
+
+ ]).flat(),
+
+ `\n`,
+
+ ] : []),
+
+ // --- React-Query Hook -------------------------------------------------------------
+
+ `## Client Usage\n`,
+
+ `\n`,
+
+ `### Custom \`react-query\` hook\n`,
+
+ `> e.g. In the \`${ctx.fetcherFileName}.ts\` file:\n`,
+
+ `\`\`\`typescript copy`,
+ `import { ${ctx.useFetcherType}, ${ctx.UseFetcherTypeOptions}, ${ctx.ResolverType}Key } from '@tanstack/react-query'`,
+ `\`\`\`\n`,
+
+ `\`\`\`typescript copy\n`,
+ ...(ctx.isQuery ? [
+ `export const ${ctx.useFetcherHookName} = (`,
+ ` input: ${ctx.FetcherInputTypeName},`,
+ ` options?: Omit<${ctx.UseFetcherTypeOptions}<${ctx.FetcherOutputTypeName}>, '${ctx.resolverType}Fn' | '${ctx.resolverType}Key'> & {`,
+ ` ${ctx.resolverType}Key?: ${ctx.ResolverType}Key,`,
+ ` },`,
+ `) => {`,
+ ` return ${ctx.useFetcherType}({`,
+ ` ${ctx.resolverType}Key: ['${ctx.fetcherName}', input],`,
+ ` ${ctx.resolverType}Fn: (context) => ${ctx.fetcherName}(input),`,
+ ` ...options,`,
+ ` })`,
+ `}`,
+ ] : [
+ `export const ${ctx.useFetcherHookName} = (`,
+ ` options?: Omit<`,
+ ` ${ctx.UseFetcherTypeOptions}<${ctx.FetcherOutputTypeName}, unknown, ${ctx.FetcherInputTypeName}>,`,
+ ` '${ctx.resolverType}Fn' | '${ctx.resolverType}Key'`,
+ ` > & { ${ctx.resolverType}Key?: ${ctx.ResolverType}Key }`,
+ `) => {`,
+ ` return ${ctx.useFetcherType}({`,
+ ` ${ctx.resolverType}Key: ['${ctx.fetcherName}'],`,
+ ` ${ctx.resolverType}Fn: (input: ${ctx.FetcherInputTypeName}) => ${ctx.fetcherName}(input),`,
+ ` ...options,`,
+ ` })`,
+ `}`,
+ ]),
+ `\`\`\`\n`,
+
+ `Be sure to check the [\`${ctx.useFetcherType}\`](https://tanstack.com/query/latest/docs/framework/react/reference/${ctx.useFetcherType}) docs for all the available options you might want to prefill or abstract.\n`,
+
+ `\n`,
+
+ // --- React Hook Usage -------------------------------------------------------------
+
+ `### Usage in React\n`,
+
+ `\`\`\`typescript copy`,
+ `import { ${ctx.useFetcherHookName} } from '${ctx.workspaceName}/resolvers/${ctx.fetcherFileName}'`,
+ `\`\`\`\n`,
+
+ ...(ctx.isQuery ? [
+ `\`\`\`typescript copy`,
+ `const { data, error, isLoading } = ${ctx.useFetcherHookName}({ ${ctx.resolverArgsName}: /* ... */ }, {`,
+ ` // ... any additional options ...`,
+ `})`,
+ `\`\`\`\n`,
+ ] : [
+ `\`\`\`typescript copy`,
+ `const { data, error, isLoading, mutateAsync } = ${ctx.useFetcherHookName}({`,
+ ` // ... any additional options ...`,
+ `})`,
+ `\`\`\`\n`,
+
+ `\`\`\`typescript copy`,
+ `const onSubmit = async (input: ${ctx.inputSchemaName}) => {`,
+ ` const response = await mutateAsync({ ${ctx.resolverArgsName}: input }, options)`,
+ ` // ?^ { ${ctx.resolverName}: ${ctx.outputSchemaName} } `,
+ `}`,
+ `\`\`\`\n`,
+ ]),
+
+ `Be sure to check the [\`${ctx.useFetcherType}\`](https://tanstack.com/query/latest/docs/framework/react/reference/${ctx.useFetcherType}) docs for all the available options.\n`,
+
+ // --- React Form State -------------------------------------------------------------
+
+ ...(ctx.isMutation ? [
+
+ `\n`,
+
+ `### Usage - Form State\n`,
+
+ `You can also use the \`${ctx.resolverName}\` input schema for react form state helpers, e.g.:\n`,
+
+ renderFileTree([`${ctx.workspacePath}/hooks/${ctx.formHookName}.ts`]),
+
+ `\`\`\`typescript copy`,
+ createBridgedFormHookContent(ctx),
+ `\`\`\`\n`,
+
+ `Check out the [Form Management Docs](/form-management) for more details on how to use this hook.\n`,
+
+ ] : []),
+
+ `\n`,
+
+ // --- Custom MDX Docs -------------------------------------------------------------
+
+ ...(!!ctx.customMdxDocs ? [
+
+ `## Developer Notes\n`,
+
+ ctx.customMdxDocs,
+
+ `\n`,
+
+ ] : []),
+
+ // --- Docgen Disclaimer ------------------------------------------------------------
+
+ `## Other\n`,
+
+ `### Disclaimer - Automatic Docgen\n`,
+
+ ``,
+ [
+ `These dynamic API docs were auto-generated with \`npm run regenerate-docs\`. `,
+ `This happens from \`.bridge.ts\` files in any \`\/resolvers\/\` folder.\n\n`,
+ `You can opt-out of this by adding \`export const optOut = true\` somewhere in the file. `,
+ ].join(''),
+ `\n`,
+
+].filter(Boolean).join('\n')
+
+/* --- regenerate-docs ------------------------------------------------------------------------- */
+
+const regenerateDocs = async () => {
+ try {
+
+ // Keep track of metadata
+ const workspaceMeta = { features: {}, packages: {} } as Record>
+ const metaFilesTree = {} as Record>
+ const addWorkspaceMeta = (workspacePath: string) => {
+ const workspaceName = PATH_PKGS[workspacePath]
+ const [workspaceType, workspaceFolder] = workspacePath.split('/')
+ workspaceMeta[workspaceType][workspaceFolder] = workspaceName
+ }
+
+ // ----------------------------------------------------------------------------------------
+ // -i- Clear existing autogenerate package / feature docs
+ // ----------------------------------------------------------------------------------------
+
+ workspacePaths.map((workspacePath) => {
+ const workspaceFolderName = workspacePath.split('/').pop()!
+ const docsFolderName = `../../apps/docs/pages/${workspaceFolderName}`
+ fs.rmSync(docsFolderName, { recursive: true, force: true })
+ })
+
+ // ----------------------------------------------------------------------------------------
+ // -i- Collect custom docs file paths
+ // ----------------------------------------------------------------------------------------
+
+ const featureMdxDocsPaths = globRel('../../features/**/*.docs.mdx')
+ const packageMdxDocsPaths = globRel('../../packages/**/*.docs.mdx')
+ const allMdxDocsPaths = [...featureMdxDocsPaths, ...packageMdxDocsPaths]
+
+ const featureReadMePaths = globRel('../../features/**/README.md')
+ const packageReadMePaths = globRel('../../packages/**/README.md')
+ const allReadMePaths = [...featureReadMePaths, ...packageReadMePaths]
+ const allCustomDocsPaths = [...allMdxDocsPaths, ...allReadMePaths]
+
+ const customDocsTree = allCustomDocsPaths.reduce((acc, customDocsPath) => {
+
+ // Skip empty files
+ const mdxContent = fs.readFileSync(customDocsPath, 'utf-8')
+ if (!mdxContent) return acc
+
+ // Skip files that have opt-out patterns
+ if (hasOptOutPatterns(mdxContent) && !customDocsPath.includes('@green-stack-core')) return acc
+
+ // Figure out the paths and workspace info
+ const filePath = customDocsPath.replaceAll('../', '') // e.g. 'features/@app-core/docs/Button.docs.mdx'
+ const innerFilePath = filePath.split('/').slice(1).join('/') // e.g. '@app-core/docs/Button.docs.mdx'
+ const fileNameWithExt = innerFilePath.split('/').pop()! // e.g. 'Button.docs.mdx'
+ const fileName = fileNameWithExt.split('.').shift()! // e.g. 'Button'
+ const workspacePath = filePath.split('/').slice(0, 2).join('/') // e.g. 'features/@app-core'
+ const workspaceName = PATH_PKGS[workspacePath] // e.g. '@app/core'
+ const workspaceFolder = workspacePath.split('/').pop()! // e.g. '@app-core'
+ const innerFileFolder = innerFilePath.split('/').slice(0, -1).join('/') // e.g. '@app-core/docs'
+
+ // Add relevant workspace meta for docs?
+ addWorkspaceMeta(workspacePath)
+
+ // Plan MDX file paths and _meta setup
+ let mdxFileFolder = `../../apps/docs/pages/${innerFileFolder}` // e.g. '../../apps/docs/pages/@app-core/docs'
+ let mdxFilePath = `${mdxFileFolder}/${fileName}.mdx` // e.g. '@app-core/docs/Button.mdx',
+ const isIndexFile = mdxFilePath.includes('index.mdx')
+ if (isIndexFile) mdxFilePath = mdxFilePath.replace('/index', '') // e.g. -> '@green-stack-core/schemas.mdx'
+ if (isIndexFile) mdxFileFolder = mdxFileFolder.split('/').slice(0, -1).join('/') // e.g. -> '../../apps/docs/pages/@green-stack-core'
+
+ // Should the docs entry be named after the file or the folder?
+ const entityName = isIndexFile ? mdxFilePath.split('/').pop()!.replace('.mdx', '') : fileName
+
+ // Add to the custom docs tree
+ return {
+ ...acc,
+ [mdxFilePath]: {
+ filePath,
+ entityName,
+ workspacePath,
+ workspaceFolder,
+ workspaceName,
+ mdxFilePath,
+ mdxFileFolder,
+ mdxContent,
+ }
+ }
+ }, {} as CustomDocsTree)
+
+ const extractCustomDocs = (mdxFilePath: string) => {
+ // Skip if no custom docs found for this path
+ const customDocs = customDocsTree[mdxFilePath]
+ if (!customDocs) return ''
+ // Extract the mdxContent
+ const mdxContent = customDocs.mdxContent
+ // Delete the entry so we don't process it again
+ delete customDocsTree[mdxFilePath]
+ // Return the mdxContent
+ return mdxContent
+ }
+
+ // ----------------------------------------------------------------------------------------
+ // -i- UI Component Docs
+ // ----------------------------------------------------------------------------------------
+
+ // Get all component file paths
+ const featureComponentPaths = globRel('../../features/**/*.tsx')
+ const packageComponentPaths = globRel('../../packages/**/*.tsx')
+ const allComponentPaths = [...featureComponentPaths, ...packageComponentPaths]
+
+ // Figure out import paths from each workspace
+ const { workspaceImports } = parseWorkspaces('../../')
+
+ // Filter out irrelevant or non-component files
+ const filteredComponentPaths = allComponentPaths.filter((componentPath) => {
+ // Exclude barrel files
+ if (componentPath.includes('/@registries/')) return false
+ if (componentPath.includes('.primitives.tsx')) return false
+ if (componentPath.includes('/index.tsx')) return false
+ if (componentPath.includes('/styled.tsx')) return false
+ // Exclude hooks
+ if (componentPath.includes('/use')) return false
+ // Exclude platform specific files
+ if (componentPath.includes('.types.tsx')) return false
+ if (componentPath.includes('.next.tsx')) return false
+ if (componentPath.includes('.expo.tsx')) return false
+ if (componentPath.includes('.web.tsx')) return false
+ if (componentPath.includes('.native.tsx')) return false
+ if (componentPath.includes('.ios.tsx')) return false
+ if (componentPath.includes('.android.tsx')) return false
+ // Check all other components for contents
+ return true
+ })
+
+ // Build component docs tree
+ const componentDocsTree = filteredComponentPaths.reduce((acc, componentPath) => {
+
+ // Read the component file contents
+ const fileContent = fs.readFileSync(componentPath, 'utf-8')
+
+ // Skip files that have opt-out patterns
+ if (hasOptOutPatterns(fileContent)) return acc
+
+ // Filter out components not hooking into getDocumentationProps()
+ if (!fileContent.includes('.documentationProps')) return acc
+ if (!fileContent.includes('export const getDocumentationProps')) return acc
+ if (fileContent.includes('// export const getDocumentationProps')) return acc
+
+ // Figure out component workspace from filename
+ const workspaceEntry = Object.entries(workspaceImports).find(([pathKey]) => {
+ return componentPath.includes(pathKey)
+ })
+
+ // Extract the component name & path info
+ const [workspacePath, componentWorkspace] = workspaceEntry!
+ const workspaceFolder = workspacePath.split('/').pop()! // e.g. '@app-core'
+ const innerFilePath = componentPath.split(workspacePath)[1].replace('.tsx', '') // e.g. '/components/Button'
+ const componentFileName = innerFilePath.split('/').pop()!
+ const componentName = componentFileName.split('.').shift()!
+
+ // Extract file and import paths
+ const rootPath = componentPath.replaceAll('../', '') // e.g. '/features/@app-core/...'
+ const importPath = swapImportAlias(`${componentWorkspace}${innerFilePath}`) // e.g. '@app-core/components/Button'
+
+ // Skip if not exported under the correct name
+ if (!fileContent.includes(`export const ${componentName}`)) {
+ console.warn(`Component '${componentName}' exports getDocumentationProps but the component itself is not a named export, skipping '${rootPath}' for automatic docgen.`)
+ return acc
+ }
+
+ // Import the documentation props config
+ const workspacePkg = PATH_PKGS[workspacePath]
+ const importPathSync = importPath.replace(componentWorkspace, workspacePkg)
+ const { getDocumentationProps } = maybeImport(importPathSync) as {
+ getDocumentationProps: ComponentDocsData['documentationProps']
+ }
+
+ // Attempt to extract the zod schema definition and type definition
+ const propsSchema = getDocumentationProps?.propSchema
+ const propsSchemaDef = propsSchema ? renderSchemaToZodDefV3(getDocumentationProps.propSchema.introspect()) : ''
+ const propsTypeDef = propsSchema ? printNode(zodToTs(propsSchema, componentName).node) : ''
+
+ // Build MDX file path
+ const mdxFileName = `${componentName}.mdx` // -> 'Button.mdx'
+ const mdxInnerFilePath = innerFilePath.replace(componentFileName, mdxFileName)
+ const mdxFilePath = `../../apps/docs/pages/${workspaceFolder}${mdxInnerFilePath}`
+ const mdxFileFolder = mdxFilePath.split('/').slice(0, -1).join('/')
+ const customMdxDocs = extractCustomDocs(mdxFilePath)
+
+ // Add component docs to meta files tree
+ metaFilesTree[mdxFileFolder] = {
+ ...metaFilesTree[mdxFileFolder],
+ [componentName]: componentName,
+ }
+
+ // Add relevant workspace meta for docs?
+ addWorkspaceMeta(workspacePath)
+
+ // Add to component tree
+ return {
+ ...acc,
+ [componentName]: {
+ rootPath,
+ componentName,
+ componentWorkspace,
+ workspaceFolder,
+ importPath,
+ componentFileName,
+ mdxFilePath,
+ mdxFileFolder,
+ propsSchemaDef,
+ propsTypeDef,
+ customMdxDocs,
+ },
+ }
+ }, {} as ComponentDocsTree)
+
+ // Write out component MDX docs files
+ await Promise.all(Object.values(componentDocsTree).map(async (ctx: ComponentDocsData) => {
+ const mdxContent = createComponentDocsContent({ ...ctx })
+ fs.mkdirSync(ctx.mdxFileFolder, { recursive: true })
+ fs.writeFileSync(ctx.mdxFilePath, mdxContent, { flag: 'w' })
+ return Promise.resolve(true)
+ }))
+
+ // ----------------------------------------------------------------------------------------
+ // -i- Data Schema Docs
+ // ----------------------------------------------------------------------------------------
+
+ await Promise.all(Object.values(availableSchemas).map(async (schemaMeta: SchemaFileMeta) => {
+
+ // Figure out the metadata for the schema file
+ const mdxFileName = `${schemaMeta.schemaName}.mdx` // -> 'Button.mdx'
+ const mdxInnerFilePath = `/schemas/${mdxFileName}` // e.g. '/@app-core/schemas/Button.mdx'
+ const mdxWorkspaceFolder = schemaMeta.workspacePath.split('/').pop()! // e.g. '@app-core'
+ const mdxFilePath = `../../apps/docs/pages/${mdxWorkspaceFolder}${mdxInnerFilePath}`
+ const mdxFileFolder = mdxFilePath.split('/').slice(0, -1).join('/')
+
+ // Figure out custom MDX docs for this schema
+ const customMdxDocs = extractCustomDocs(mdxFilePath)
+
+ // @ts-ignore
+ const { node: tsNode } = zodToTs(schemaMeta.schema, schemaMeta.schemaName)
+ const schemaTypeDef = printNode(tsNode) // @ts-ignore
+ const schemaIntrospection = schemaMeta.schema!.introspect?.() as Meta$Schema
+ const schemaZodObjectDef = renderSchemaToZodDefV3(schemaIntrospection)
+ const mdxContent = createSchemaDocs({
+ ...schemaMeta,
+ schemaIntrospection,
+ schemaZodObjectDef,
+ schemaTypeDef,
+ customMdxDocs,
+ })
+
+ // Add relevant workspace meta for docs?
+ addWorkspaceMeta(schemaMeta.workspacePath)
+
+ // Add schema docs to meta files tree
+ metaFilesTree[mdxFileFolder] = {
+ ...metaFilesTree[mdxFileFolder],
+ [schemaMeta.schemaName]: schemaMeta.schemaName,
+ }
+
+ // Save the schema MDX docs file
+ if (!fs.existsSync(mdxFileFolder)) fs.mkdirSync(mdxFileFolder, { recursive: true })
+ fs.writeFileSync(mdxFilePath, mdxContent, { flag: 'w' })
+
+ // End the promise
+ return Promise.resolve(true)
+ }))
+
+ // ----------------------------------------------------------------------------------------
+ // -i- API Resolver Docs
+ // ----------------------------------------------------------------------------------------
+
+ await Promise.all(Object.values(availableDataBridges).map(async (bridgeMeta: BridgeFileMeta) => {
+
+ // @ts-ignore
+ const { node: tsInputNode } = zodToTs(bridgeMeta.bridge.inputSchema, bridgeMeta.inputSchemaName)
+ const inputSchemaType = printNode(tsInputNode) // @ts-ignore
+ const inputSchemaMeta = bridgeMeta.bridge.inputSchema.introspect?.() as Meta$Schema
+ const inputSchemaDef = renderSchemaToZodDefV3(inputSchemaMeta)
+
+ // @ts-ignore
+ const { node: tsOutputNode } = zodToTs(bridgeMeta.bridge.outputSchema, bridgeMeta.outputSchemaName)
+ const outputSchemaType = printNode(tsOutputNode) // @ts-ignore
+ const outputSchemaMeta = bridgeMeta.bridge.outputSchema.introspect?.() as Meta$Schema
+ const outputSchemaDef = renderSchemaToZodDefV3(outputSchemaMeta)
+
+ // @ts-ignore
+ const graphqlQueryDef = bridgeMeta.bridge?.getGraphqlQuery?.(true, false) || ''
+
+ // Include parsed metadata from the resolver generator
+ const parsedBridgeMeta = gen.parseAnswers({
+ workspacePath: bridgeMeta.workspacePath,
+ resolverName: bridgeMeta.resolverName,
+ resolverType: bridgeMeta.resolverType,
+ resolverDescription: '',
+ inputSchemaTarget: bridgeMeta.inputSchemaName,
+ inputSchemaName: bridgeMeta.inputSchemaName,
+ outputSchemaTarget: bridgeMeta.outputSchemaName,
+ outputSchemaName: bridgeMeta.outputSchemaName, // @ts-ignore
+ apiPath: bridgeMeta.bridge?.apiPath || '',
+ generatables: bridgeMeta.allowedMethods,
+ formHookName: `use${uppercaseFirstChar(bridgeMeta.resolverName)}FormState`,
+ })
+
+ // Figure out the metadata for the resolver file
+ const mdxFileName = `${bridgeMeta.resolverName}.mdx` // -> 'Button.mdx'
+ const mdxInnerFilePath = `/resolvers/${mdxFileName}` // e.g. '/@app-core/resolvers/Button.mdx'
+ const mdxWorkspaceFolder = bridgeMeta.workspacePath.split('/').pop()! // e.g. '@app-core'
+ const mdxFilePath = `../../apps/docs/pages/${mdxWorkspaceFolder}${mdxInnerFilePath}`
+ const mdxFileFolder = mdxFilePath.split('/').slice(0, -1).join('/')
+
+ // Figure out custom MDX docs for this resolver
+ const customMdxDocs = extractCustomDocs(mdxFilePath)
+
+ // Build MDX content for the resolver
+ const mdxContent = createResolverDocs({
+ ...parsedBridgeMeta,
+ ...bridgeMeta, // @ts-ignore
+ resolverArgsName: bridgeMeta.bridge?.resolverArgsName,
+ inputSchemaDef,
+ inputSchemaType,
+ outputSchemaDef,
+ outputSchemaType,
+ graphqlQueryDef,
+ customMdxDocs,
+ })
+
+ // Add relevant workspace meta for docs?
+ addWorkspaceMeta(bridgeMeta.workspacePath)
+
+ // Add resolver docs to meta files tree
+ metaFilesTree[mdxFileFolder] = {
+ ...metaFilesTree[mdxFileFolder],
+ [bridgeMeta.resolverName]: bridgeMeta.resolverName,
+ }
+
+ // Save the resolver MDX docs file
+ if (!fs.existsSync(mdxFileFolder)) fs.mkdirSync(mdxFileFolder, { recursive: true })
+ fs.writeFileSync(mdxFilePath, mdxContent, { flag: 'w' })
+
+ // End the promise
+ return Promise.resolve(true)
+ }))
+
+ // ----------------------------------------------------------------------------------------
+ // -i- Save Remaining Custom Docs
+ // ----------------------------------------------------------------------------------------
+
+ await Promise.all(Object.values(customDocsTree).map(async (customDocs: CustomDocsData) => {
+ // Extract the related metadata
+ const { mdxFilePath, mdxFileFolder, mdxContent, entityName } = customDocs
+ // Add resolver docs to meta files tree
+ metaFilesTree[mdxFileFolder] = {
+ ...metaFilesTree[mdxFileFolder],
+ [entityName]: entityName,
+ }
+ // Save the custom MDX docs file
+ if (!fs.existsSync(mdxFileFolder)) fs.mkdirSync(mdxFileFolder, { recursive: true })
+ fs.writeFileSync(mdxFilePath, mdxContent, { flag: 'w' })
+ }))
+
+ // ----------------------------------------------------------------------------------------
+ // -i- Meta Files & Wrap-Up
+ // ----------------------------------------------------------------------------------------
+
+ // Write out meta files
+ await Promise.all(Object.entries(metaFilesTree).map(async ([folderPath, mdxDocNames]) => {
+ const metaFileEntries = Object.keys(mdxDocNames).map((mdxDocName) => {
+ return ` '${mdxDocName}': '${mdxDocName}',`
+ })
+ const metaFileContent = `\nexport default {\n${metaFileEntries.join('\n')}\n}\n`
+ const metaFilePath = `${folderPath}/_meta.ts`
+ if (!fs.existsSync(folderPath)) fs.mkdirSync(folderPath, { recursive: true })
+ fs.writeFileSync(metaFilePath, metaFileContent, { flag: 'w' })
+ return Promise.resolve(true)
+ }))
+
+ // Write package aliases to registries
+ const hasFeatureMeta = Object.keys(workspaceMeta.features).length > 0
+ const hasPackageMeta = Object.keys(workspaceMeta.packages).length > 0
+ const featureMeta = hasFeatureMeta ? JSON.stringify(workspaceMeta.features, null, 4) : '{}'
+ const featureMetaLines = `export const featureMeta = ${featureMeta}`
+ const packageMeta = hasPackageMeta ? JSON.stringify(workspaceMeta.packages, null, 4) : '{}'
+ const packageMetaLines = `export const packageMeta = ${packageMeta}`
+ const workspaceImportsFile = [featureMetaLines, packageMetaLines].join('\n\n')
+ fs.writeFileSync('../../packages/@registries/workspaceImports.generated.ts', workspaceImportsFile)
+
+ } catch (err) {
+ console.error(err)
+ process.exit(1)
+ }
+}
+
+/* --- init ------------------------------------------------------------------------------------ */
+
+regenerateDocs()
diff --git a/apps/docs/tailwind.config.js b/apps/docs/tailwind.config.js
new file mode 100644
index 0000000..41999d5
--- /dev/null
+++ b/apps/docs/tailwind.config.js
@@ -0,0 +1,17 @@
+const { universalTheme } = require('@app/core/tailwind.theme.js')
+
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ darkMode: 'class',
+ content: [
+ '../../apps/**/*.{js,jsx,ts,tsx,md,mdx}',
+ '../../features/**/*.{js,jsx,ts,tsx,md,mdx}',
+ '../../packages/**/*.{js,jsx,ts,tsx,md,mdx}',
+ ],
+ presets: [require('nativewind/preset')],
+ important: 'html',
+ theme: {
+ ...universalTheme,
+ },
+ plugins: [require('tailwindcss-animate')],
+}
diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json
new file mode 100644
index 0000000..613ad68
--- /dev/null
+++ b/apps/docs/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "extends": "@app/core/tsconfig",
+ "include": [
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ "../../apps/next/next-env.d.ts",
+ "../../packages/@green-stack-core/global.d.ts",
+ "../../features/@app-core/nativewind-env.d.ts",
+ "../../features/@app-core/appConfig.ts",
+ "../../apps/docs/**/*.ts",
+ "../../apps/docs/**/*.tsx",
+ "../../features/**/*.ts",
+ "../../features/**/*.tsx",
+ "../../packages/**/*.ts",
+ "../../packages/**/*.tsx"
+ ],
+ "compilerOptions": {
+ "jsxImportSource": "nativewind",
+ "plugins": [{
+ "name": "next"
+ }]
+ },
+ "exclude": [
+ "node_modules"
+ ]
+}
diff --git a/apps/expo/app/(generated)/demos/markdown/index.tsx b/apps/expo/app/(generated)/demos/markdown/index.tsx
new file mode 100644
index 0000000..765b05f
--- /dev/null
+++ b/apps/expo/app/(generated)/demos/markdown/index.tsx
@@ -0,0 +1,2 @@
+// -i- Automatically generated by 'npx turbo @green-stack/core#link:routes', do not modify manually, it will get overwritten
+export { default } from '@app/core/routes/demos/markdown/index'
diff --git a/apps/expo/metro.config.js b/apps/expo/metro.config.js
index 41873be..d02fc09 100644
--- a/apps/expo/metro.config.js
+++ b/apps/expo/metro.config.js
@@ -20,5 +20,9 @@ config.resolver.nodeModulesPaths = [
// 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths`
// config.resolver.disableHierarchicalLookup = true
+// 4. Add .md & .mdx files to the file extensions Metro will handle
+config.resolver.sourceExts.push('md', 'mdx')
+config.transformer.babelTransformerPath = require.resolve('./transformer.js')
+
// Export the modified config
module.exports = withNativeWind(config, { input: '../next/global.css' })
diff --git a/apps/expo/package.json b/apps/expo/package.json
index c89fa03..2de42aa 100644
--- a/apps/expo/package.json
+++ b/apps/expo/package.json
@@ -4,6 +4,7 @@
"private": true,
"main": "index.js",
"dependencies": {
+ "@bacons/mdx": "~0.2.0",
"@expo/metro-runtime": "~4.0.0",
"expo": "~52.0.11",
"expo-constants": "~17.0.3",
diff --git a/apps/expo/transformer.js b/apps/expo/transformer.js
new file mode 100644
index 0000000..54cf9e9
--- /dev/null
+++ b/apps/expo/transformer.js
@@ -0,0 +1,10 @@
+const upstreamTransformer = require('@expo/metro-config/babel-transformer')
+const MdxTransformer = require("@bacons/mdx/metro-transformer")
+
+module.exports.transform = async (props) => {
+ // Then pass it to the upstream transformer.
+ return upstreamTransformer.transform(
+ // Transpile MDX first.
+ await MdxTransformer.transform(props)
+ )
+}
diff --git a/apps/next/app/(generated)/demos/markdown/page.tsx b/apps/next/app/(generated)/demos/markdown/page.tsx
new file mode 100644
index 0000000..40200c7
--- /dev/null
+++ b/apps/next/app/(generated)/demos/markdown/page.tsx
@@ -0,0 +1,2 @@
+"use client"
+export { default } from '@app/core/routes/demos/markdown/index'
diff --git a/apps/next/next.config.base.cjs b/apps/next/next.config.base.cjs
index 9ce3484..45d19d7 100644
--- a/apps/next/next.config.base.cjs
+++ b/apps/next/next.config.base.cjs
@@ -9,6 +9,9 @@ const mainNextConfig = {
"expo",
"expo-constants",
"expo-modules-core",
+ "@bacons/mdx",
+ "@bacons/react-views",
+ "@expo/html-elements",
"@rn-primitives/hooks",
"@rn-primitives/slot",
"@rn-primitives/portal",
diff --git a/apps/next/next.config.js b/apps/next/next.config.js
index 4e9268c..b14992a 100644
--- a/apps/next/next.config.js
+++ b/apps/next/next.config.js
@@ -1,7 +1,9 @@
const { withExpo } = require("@expo/next-adapter");
+const withMDX = require("@next/mdx")();
+
const mainNextConfig = require("./next.config.base.cjs");
/** @type {import('next').NextConfig} */
-const nextConfig = withExpo(mainNextConfig);
+const nextConfig = withMDX(withExpo(mainNextConfig));
module.exports = nextConfig;
diff --git a/apps/next/package.json b/apps/next/package.json
index 8cf1e4a..1036051 100644
--- a/apps/next/package.json
+++ b/apps/next/package.json
@@ -3,6 +3,10 @@
"version": "1.0.0",
"private": true,
"dependencies": {
+ "@mdx-js/loader": "^3.1.0",
+ "@mdx-js/react": "^3.1.0",
+ "@next/mdx": "~15.0.4",
+ "@types/mdx": "^2.0.13",
"next": "15.0.4"
},
"devDependencies": {
diff --git a/features/@app-core/constants/testableFeatures.ts b/features/@app-core/constants/testableFeatures.ts
index eac16c3..bba6bda 100644
--- a/features/@app-core/constants/testableFeatures.ts
+++ b/features/@app-core/constants/testableFeatures.ts
@@ -8,4 +8,8 @@ export const testableFeatures = [
title: 'Test Forms?',
link: '/demos/forms'
},
+ {
+ title: 'Test Markdown?',
+ link: '/demos/markdown'
+ }
]
diff --git a/features/@app-core/mdx/CodeBlock.tsx b/features/@app-core/mdx/CodeBlock.tsx
new file mode 100644
index 0000000..8da9e31
--- /dev/null
+++ b/features/@app-core/mdx/CodeBlock.tsx
@@ -0,0 +1,54 @@
+"use client"
+import { useState, useEffect } from 'react'
+import { Pre, Code } from 'nextra/components'
+import { cn } from '@app/primitives'
+import { codeToHtml, BundledLanguage } from 'shiki'
+
+/* --- Types ----------------------------------------------------------------------------------- */
+
+export type CodeBlockProps = {
+ code: string,
+ lang?: BundledLanguage | HintedKeys,
+ className?: string,
+ children?: React.ReactNode,
+}
+
+/* --- ---------------------------------------------------------------------------- */
+
+export const CodeBlock = (props: CodeBlockProps) => {
+ // Props
+ const { code, lang = 'jsx', className, children } = props
+
+ // State
+ const [highlightedCode, setHighlightedCode] = useState(null)
+
+ // Vars
+ const bgColor = highlightedCode?.split('style="background-color:')[1]?.split(';')[0] || ''
+
+ // -- Effects --
+
+ useEffect(() => {
+ const highlightCode = async () => {
+ const renderedCode = await codeToHtml(code, {
+ lang,
+ theme: 'dark-plus',
+ })
+ setHighlightedCode(renderedCode)
+ }
+ highlightCode()
+ }, [code])
+
+ // -- Render --
+
+ return (
+