# Introduction Folioso is a lightweight knowledge base for developers: rich-text notes, syntax-highlighted code, diagrams, images and easy sharing in one web app. This page explains what it does and where to start. ## What Folioso is for Folioso is the place you open when you think "I need to write this down". It's deliberately simpler than a full wiki, and it's built around the things developers actually keep: - **Notes and how-tos** in a clean editor with headings, lists, task lists, tables and alert boxes. - **Code** in highlighted code blocks with line numbers, a language picker and a copy button. - **Diagrams** written as Mermaid or PlantUML source and drawn on the page. - **Images** you paste, drop or upload, resized and captioned in place. - **Links between pages**, so your notes build on each other. Everything saves as you type, and when colleagues open the same page you edit it together in real time. ![A page open in the editor with its formatting toolbar](/images/docs/screens/editor-light.webp) ## How it's organised Your work lives in an **organisation**, which has its own address. Inside it, pages sit in **spaces**, and you can sort a space's pages into **groups**. Every member also gets a private **My space** for their own notes. Spaces are also how access works: each one decides who can read and edit what's in it. See [Key concepts](/getting-started/key-concepts) for the full picture. ## Where to start - New to Folioso? Follow the [Quick start](/getting-started/quick-start) to create your account and write your first page. - Want to understand the building blocks first? Read [Key concepts](/getting-started/key-concepts). - Looking for something on screen? [Finding your way around](/getting-started/navigating) is a tour of the sidebar, the page header and search. - Ready to write? Start with [Creating and managing pages](/editor/pages) and keep [Keyboard shortcuts](/editor/keyboard-shortcuts) handy. ## Plans Folioso has four plans: Free, Pro, Team and Business. Every plan includes the editor, code blocks, diagrams, images, tags and real-time collaboration. Paid plans raise the limits and add features such as version history and PDF export. See [Plans](/billing/plans) for how plans work and the [pricing page](https://folioso.dev/pricing) for what each one includes. --- # Quick start Create your Folioso account, set up your organisation and write your first page in a few minutes. This page walks through registration, the plan step and what you'll find when you first sign in. ## Create your account 1. Go to [folioso.dev/register](https://folioso.dev/register). 2. Fill in **Your name**, **Work email** and **Organisation**. The organisation name becomes the name of your workspace. You can rename it later. 3. Choose a **Password**. It needs at least 8 characters, with a lowercase letter, an uppercase letter, a number and a symbol. The form ticks each rule off as you type. 4. Re-enter it under **Confirm password** and click **Create account**. ![The registration form, with the password rules listed under Password](/images/docs/screens/register-light.webp) ### Sign up with GitHub instead Click **Continue with GitHub** on the registration or sign-in page. Folioso uses the verified primary email address on your GitHub account, so you don't need to verify it again, and creates a workspace named after you, such as "Ada's Workspace". You can rename it later. If your GitHub account has no verified email address, sign-up stops and asks you to verify one on GitHub first. If you already have a Folioso account with the same verified address, GitHub is linked to that account instead of creating a new one. See [Signing in](/account/signing-in#sign-in-with-github). > [!NOTE] > An account created with GitHub has no password until you set one. You can always sign in with **Continue with GitHub**. ## Pick a plan After **Create account**, the **Plan** step asks you to choose a plan for your new organisation. Everyone starts on Free, and you can change plan whenever you like from the organisation's Billing page. Click **Continue to Folioso** to finish. ![The plan step of registration](/images/docs/screens/choose-plan-light.webp) For how plans work, see [Plans](/billing/plans), and for what each plan includes, see the [pricing page](https://folioso.dev/pricing). For what happens when you change plan later, see [Choosing a plan](/billing/choosing-a-plan). You skip the plan step when you sign up to join an organisation that invited you, or with a sign-up invite that already includes a plan. ## If you were invited When an organisation admin invites you, you get an email with a link. Open it and, if you're new to Folioso, create an account with the address the invitation was sent to. The form fills it in for you and won't let you change it. You join the inviting organisation straight away and land in it. Because you opened the link from your inbox, your email address counts as verified. Folioso also sets up a workspace of your own alongside it, shown on the form as "You're joining … We'll also set up your own workspace". Click **Change** to rename that workspace before you create the account. Invitations expire after 7 days. If yours has expired, ask an admin to resend it. See [Members and invitations](/organisation/members#what-the-invited-person-does). ## If sign-ups are invite-only At times Folioso only accepts new accounts by invitation. The registration page then shows **Folioso is invite-only for now**. Leave your email and click **Register interest** to hear when a place opens up. You can still create an account while sign-ups are closed if you have: - an invitation to join an organisation, or - a sign-up invite link. Opening it takes you to the registration form, which may say "You've been invited to try Folioso on the … plan, free." While sign-ups are closed, paid plans on the plan step may be marked **Invite only**. Start on Free, and use **Register interest** on that step to hear when they open. ## Verify your email address Folioso emails you a verification link when you register with a password. You can use the app straight away, but until you verify you can't share pages, turn on public links or invite people. A banner reminds you: "Verify your email address to share pages and invite people." Click **Verify now** to open the verification page. See [Signing in](/account/signing-in#verify-your-email-address). ## What you get Your new organisation comes with: - **You as its admin and owner.** You manage members, spaces and billing. - **A space called General.** It's the organisation's default space and it's open, so everyone you invite can read and edit its pages. - **My space.** Your own private space. Nobody else can see it, including other admins. - **The plan you picked**, or Free. - **A welcome page** in General, with a short introduction and a sample code block. It's an ordinary page, so edit or delete it whenever you like. You land in the General space. Your organisation has its own address, such as `org123456.folioso.dev`. If your plan includes a custom address, you can choose a memorable one. See [Organisation settings](/organisation/settings). ## Write your first page 1. Click **New Page** in the sidebar. A blank page opens in the editor. 2. Type a title where it says **Untitled**, then press Enter to move into the page. 3. Start writing. Type `/` at the start of a line to insert a heading, code block, task list, table, image, diagram or alert box. 4. Stop typing and Folioso saves your changes about 3 seconds later. The header shows **Saving…** and then **Saved**. ![A page open in the editor](/images/docs/screens/editor-light.webp) Next, try: - [Code blocks](/editor/code-blocks) for highlighted snippets. - [Tags](/editor/tags) to label the page, for example by language. - [Groups](/spaces/groups) to sort the space's pages into folders. - [Sharing with people](/sharing/sharing-with-people) to send the page to someone. ## Invite your team If you're an organisation admin, open the organisation and space switcher at the top of the sidebar and choose **Members** to invite people by email. See [Members and invitations](/organisation/members). Admins of an organisation with other members must use two-factor authentication, so you may be asked to set it up first. See [Two-factor authentication](/account/two-factor-authentication). --- # Key concepts Folioso is built from a few simple pieces: organisations, spaces, groups, pages, teams and roles. This page explains each one and how they decide who can see and edit what. ## How the pieces fit together ```text Organisation (your-team.folioso.dev) ├── Members and teams ├── General (the default space, open to every member) │ ├── Group: Runbooks │ │ └── Page │ │ └── Sub-page │ └── Page with no group ├── Other spaces (open, closed or private) └── My space (one per member, private to them) ``` ## Organisation An organisation is your team's home in Folioso. It has its own address, such as `acme.folioso.dev`, and it owns everything inside it: the spaces, the teams, the members and the plan. - Every account has its own organisation, created when you register. - You can belong to several organisations, for example your own and your employer's. You switch between them from the top of the sidebar. See [Multiple organisations](/organisation/multiple-organisations). - The plan belongs to the organisation, not to you. See [Plans](/billing/plans). ### Organisation roles Each member is either an **admin** or a **member**. | Role | What they can do | | --- | --- | | **Admin** | Everything a member can, plus manage members, teams, spaces, settings and billing. Admins are admins of every space except other people's My space. | | **Member** | Use the spaces they have access to. | One admin is also the **owner**: the organisation's billing contact, who can't be removed while they hold that role. See [Roles and ownership](/organisation/roles-and-ownership). ## Space A space holds pages and groups, and it's where access is decided. Every space has an access type: - **Open:** every member of the organisation can read and edit it. - **Closed:** listed for every member, but only the people and teams granted a role can get in. Others can ask for access. - **Private:** hidden from everyone without a grant. Available if your plan includes private spaces. Two spaces are special: - **The default space**, named **General** when the organisation is created. It's always open and can't be deleted, so everyone has somewhere to land. - **My space.** Every member gets one private personal space in each organisation they belong to. Only you can open it, organisation admins included. You can still share single pages from it. See [About spaces](/spaces/overview). ### Space roles Your role in a space decides what you can do with its pages. | Role | Read pages | Write and edit pages | Rename, move, tag and share pages | Manage the space | | --- | --- | --- | --- | --- | | **Viewer** | ✓ | — | — | — | | **Editor** | ✓ | ✓ | ✓ | — | | **Admin** | ✓ | ✓ | ✓ | ✓ | Your role can come from a direct grant, from a team you're in, from the space being open (everyone is an editor) or from being an organisation admin. When several apply, you get the highest. Only a page's author or a space admin can delete a page. See [Roles and permissions](/spaces/permissions). ## Team A team is a named group of members, such as "Backend" or "Support". Granting a team a role on a space gives that role to everyone in it, so you don't have to add people one by one. See [Teams](/organisation/teams). ## Group A group is a folder inside a space, with a name and a colour. Groups are flat: a group can't contain another group. Each page sits in one group or in none. See [Groups](/spaces/groups). ## Page A page is a single note: a title and a body that can hold text, code blocks, diagrams, tables, images and alert boxes. Pages save automatically as you type. See [Creating and managing pages](/editor/pages). - **Sub-pages.** You can nest a page under another, up to three levels deep. - **Visibility.** A new page is visible to everyone in its space, with the access their space role gives them. The author or a space admin can make a page **Private**, so only the author and the people it's shared with can open it. See [Private pages](/spaces/permissions#private-pages). - **Author.** The person who created the page. If they leave Folioso, the page stays and shows "Former member". ## Tag A tag is a coloured label on a page, such as `php` or `incident`. Tags belong to a space, and every space starts with a tag for each common code language. See [Tags](/editor/tags). ## Sharing You can share a single page without giving someone access to the whole space: - **With a person**, by email, to view or to edit. See [Sharing with people](/sharing/sharing-with-people). - **With a public link** that anyone can open, optionally with a password. See [Public links](/sharing/public-links). ## Recycle bin Deleting a page moves it to the recycle bin, where it can be restored for 30 days before it's removed for good. See [Recycle bin](/spaces/recycle-bin). --- # Finding your way around A tour of the Folioso interface: the sidebar, the organisation and space switcher, the All Pages list, the page header and the command palette. Use it to find where things live. ## The layout Every screen has the same two parts: - **The sidebar** on the left, for moving between spaces, groups and pages. - **The main area** on the right, with a header bar across the top. On a narrow screen the sidebar is hidden. Tap **Open navigation** in the header to slide it in, and **Close navigation** to put it away. On a wide screen, **Collapse sidebar** at the top shrinks it to a strip of icons and **Expand sidebar** brings it back. ![A space's All Pages home in list view, with the sidebar on the left showing the space's groups](/images/docs/screens/space-home-light.webp) ## The sidebar From top to bottom: | Item | What it does | | --- | --- | | **Organisation and space switcher** | Shows the organisation and the space you're in. Click it to switch. See [Switching spaces and organisations](#switching-spaces-and-organisations). | | **Search...** | Opens the command palette. The shortcut is shown beside it. | | **New Page** | Creates a page in the current space and opens it. Greyed out if you can only view the space. | | **All Pages** | The space's home page, listing every page in the space you can open, with a count. | | **Activity** | Recent changes in the space. See [Space activity](/spaces/activity). | | **Favourites** | Pages you've starred. It only appears once you have at least one. | | **Groups** | Each of the space's groups, with its pages underneath. Pages with no group are listed under **Ungrouped**. | | **Shared** | Expands to **Shared with Me**, **Opened from links** and **Shared by me**. See [Shared with you](/sharing/shared-pages). | | **Recycle bin** | Pages you deleted, and pages deleted in spaces you administer. See [Recycle bin](/spaces/recycle-bin). | | **Your name** | At the bottom, with your plan, a **Settings** button and **Sign out**. | ### Working with pages in the sidebar - Click a page to open it. Click the arrow beside a page to show or hide its sub-pages. - Hover over a page and click **More actions** (or right-click it) for **Rename**, **Make child page**, **Promote page**, **Move up**, **Move down** and **Delete**. You only see the actions you're allowed to use. - Drag a page to reorder it, nest it or move it to another group. - If you can edit the space, a group's **More actions** menu has **Add page**, **Edit** and **Delete**. See [Creating and managing pages](/editor/pages) and [Groups](/spaces/groups). ### Favourites Star a page with the star in its header to add it to **Favourites**. Click **Favourites** in the sidebar to open the list, type in **Search favourites** to filter it, and click a page to open it. Favourites follow you across spaces and organisations, and a favourite from elsewhere shows which space it's in. Star the page again to remove it. You can favourite any page you can open, including pages shared with you. ## Switching spaces and organisations Click the organisation and space name at the top of the sidebar. The panel that opens lists: - **Organisations**, if you belong to more than one. Click one to switch. Each organisation has its own address, so the page reloads on the new address. See [Multiple organisations](/organisation/multiple-organisations). - **My space** and then **Spaces**: the spaces you can open. The default space is marked **Default**, and closed or private spaces are labelled with their access type. Click a space to go to it. - **Browse spaces**, the directory of every open and closed space, where you can ask to join a closed one. See [Requesting access](/spaces/access-requests). - **New space** (organisation admins) and **New group**. - For organisation admins, links to the organisation hub: **Overview**, **Members**, **Teams**, **Spaces**, **Billing**, **Recycle Bin**, **Audit log** and **Settings**. Space admins who aren't organisation admins see **Spaces**. See [Organisation overview](/organisation/overview). - **Leave organisation**, when you're allowed to leave. When you open an organisation, Folioso takes you to the last space you visited if it's in that organisation, and otherwise to its default space. ![The Spaces directory, listing open and closed spaces with their access type, member count and Go to space](/images/docs/screens/spaces-directory-light.webp) ## All Pages **All Pages** is a space's home page. It shows recent activity in the space and then every page you can open there. The buttons in its header: - **New page** creates a page. It's shown on narrow screens only; on a wide screen, use **New Page** in the sidebar. - **Newest first** / **Oldest first** changes the order. - **List view** / **Grid view** switches the layout. The list shows sub-pages indented under their parent. ![All Pages in grid view, with recent activity above the page cards](/images/docs/screens/all-pages-light.webp) Each page has a **Page actions** menu (also on right-click) with **Export as PDF**, **Export as Markdown**, **Duplicate page**, **Move to group…**, **Move to space…** and **Delete**, depending on what you're allowed to do. A lock icon marks a private page, and a globe icon marks a page with a public link. ## The page header When you open a page, the header shows: - **Back** and the page's group and title. - **Saving…**, **Saved** or **Unsaved changes**, so you know your work is safe. - The faces of everyone else on the page, when you're [editing together](/editor/collaboration). - The page's word count. - The star for **Favourites**. - The notifications bell. See [Notifications](/sharing/notifications). - A theme button to switch between light, dark and system. - **Private** or **Public**, if the page isn't simply visible to its space, and **Share**, if you can share it. - **More actions**, the page's menu. See [Creating and managing pages](/editor/pages#the-page-menu). ![The More actions menu open in the page header](/images/docs/screens/page-actions-light.webp) ## The command palette Press **Ctrl K** (**⌘K** on a Mac) anywhere, or click **Search...** in the sidebar, to open the command palette. - Before you type, it lists your **Recent** pages: the five most recently edited pages in the current space. - As you type, it searches the titles and content of every page you can open in the organisation and shows **Results**. - Use the arrow keys to move, Enter to open a page and Escape to close. See [Search](/sharing/search). ## Settings Click **Settings** at the bottom of the sidebar for your profile, password and two-factor authentication, editor preferences, tags, API tokens, data export and account deletion. See [Profile](/account/profile) and [Preferences](/account/preferences). --- # Creating and managing pages Pages are where you write in Folioso. This page covers creating a page, how saving works, and how to rename, nest, favourite, duplicate, move and delete pages. ## Create a page You need the Editor or Admin role in the space. In an open space, every member has it. - Click **New Page** in the sidebar to create a page in the current space, with no group. - To create a page inside a group, open the group's **More actions** menu in the sidebar and choose **Add page**. - On **All Pages** on a narrow screen, click **New page** in the header. The new page opens straight away in the editor, titled **Untitled**. It's visible to everyone in the space, with whatever access their role gives them. To keep it to yourself, make it private from **Share**. See [Private pages](/spaces/permissions#private-pages). ## Write the page 1. Click **Untitled** and type the page's title. Press Enter to jump to the start of the body. 2. Write in the body. Use the toolbar above the page, Markdown-style shortcuts such as `## ` for a heading, or type `/` at the start of a line for the block menu. See [Formatting text](/editor/formatting). The line under the title shows who last edited the page and when, plus the page's tags. The header shows the word count. ![A page open in the editor, with the toolbar above it, tags under the title, and the word count and save status in the header](/images/docs/screens/editor-light.webp) If you can view the page but not edit it, the toolbar is replaced by **Read-only** and "You don't have permission to edit this page". ### The / block menu Type `/` at the start of a line to open the block menu. Keep typing to filter it, use the arrow keys to choose, and press Enter or Tab to insert. Press Escape to close it. | Item | Inserts | | --- | --- | | **Heading 1** | A large section heading | | **Code block** | A syntax-highlighted code block. See [Code blocks](/editor/code-blocks). | | **Task list** | A checkbox to-do list | | **Table** | A 3×3 table with a header row | | **Image** | Opens the file picker to upload an image. See [Images](/editor/images). | | **Mermaid diagram** | A diagram block, Mermaid or PlantUML. See [Diagrams](/editor/diagrams). | | **Info box**, **Success box**, **Warning box**, **Error box** | A titled alert box. See [Alert boxes](/editor/alerts). | The menu doesn't open inside a code block, inline code, a table or an alert box. ## Saving There's no save button. Folioso saves your changes about 3 seconds after you stop typing. The header tells you where things stand: | Status | Meaning | | --- | --- | | **Saved** | Everything you've typed is stored. | | **Saving…** | A save is on its way. | | **Unsaved changes** | You've typed since the last save, or the last save didn't go through. | To save immediately, press **Ctrl S** (**⌘S** on a Mac). Leaving the page also saves any pending changes first. If someone changes your access while you're editing, so that your changes can't be saved, a **Your access changed** dialog explains what happened. Copy anything you need from the screen, then click **Reload page** to see the current version. ## The page menu Click **More actions** in the page header for the page's menu. It appears for anyone with the Editor or Admin role in the space, and for the page's author. People who can open the page only because it was shared with them don't get it. ![The More actions menu open in the page header, from Export as PDF down to Delete](/images/docs/screens/page-actions-light.webp) | Item | What it does | | --- | --- | | **Export as PDF** | Builds a PDF of the page. Marked **Pro** on plans without it. See [Exporting pages](/editor/exporting). | | **Export as Markdown** | Downloads the page as a `.md` file. | | **Snapshots** | Opens the page's version history. Marked **Pro** on plans without it. See [Version history](/editor/version-history). | | **Duplicate page** | Makes a copy of the page. | | **Move to group…** | Moves the page to another group in the space, or to **No group**. | | **Move to space…** | Moves the page to another space. | | **Delete** | Moves the page to the recycle bin. Only for the author and space admins. | Each page's **Page actions** menu on **All Pages** has the same actions apart from **Snapshots**. ## Rename a page - In the editor, click the title and type. The new title saves with the rest of the page. Anyone who can edit the page can do this. - In the sidebar, open the page's **More actions** menu and choose **Rename**. Type the new name and press Enter, or press Escape to cancel. This needs the Editor or Admin role in the space, or authorship of the page. Titles can be up to 255 characters. A page with no title shows as **Untitled**. ## Sub-pages You can nest pages under other pages, up to **three levels** deep. Nested pages appear indented under their parent in the sidebar and on **All Pages**, and the editor lists a page's children under **Sub-pages** beside the text. In the sidebar, open a page's **More actions** menu: - **Make child page** nests the page under the page above it. - **Promote page** moves the page up one level, out from under its parent. - **Move up** and **Move down** swap the page with its neighbour at the same level, taking any sub-pages with it. You can also drag pages in the sidebar: drop a page next to a nested page to nest it alongside that page, or next to a top-level page to bring it back to the top level. Rearranging pages needs the Editor or Admin role in the space, or authorship of the page. Deleting a page doesn't delete its sub-pages. They stay in the space and appear at the top level of their group. ## Favourite a page Click the star in the page header to add the page to **Favourites** in the sidebar, and click it again to remove it. You can favourite any page you can open. See [Favourites](/getting-started/navigating#favourites). ## Duplicate a page Choose **Duplicate page** from the page menu. Folioso saves your latest changes, then creates a copy and opens it. The copy: - is named after the original with " copy" on the end; - sits in the same space and group, at the end of the list, and at the top level rather than under the original's parent; - has the same content, tags and visibility; - has you as its author, starts with no sharing, no sub-pages and no version history. ## Move a page ### To another group Choose **Move to group…** and pick a group, or **No group**. You can also drag the page onto a group in the sidebar. Sub-pages move with it. ### To another space 1. Choose **Move to space…**. 2. Pick the **Space**, and optionally a **Group** in it. 3. Click **Move page**. The page and every page nested under it move together. The page keeps its address, so links to it keep working, and it lands at the top level of the space you chose. Its tags come with it: each one is matched to a tag of the same name in the new space, or created there. You can only move a page to a space where you have the Editor or Admin role. If there's none, the dialog says "There is no other space you can add pages to." > [!NOTE] > Moving a page to another space changes who can see it. Snapshots taken before the move are hidden from everyone, so earlier versions don't reach the new audience. See [Version history](/editor/version-history#who-can-see-snapshots). ## Delete a page Only the page's author and space admins, including organisation admins, can delete a page. 1. Choose **Delete** from the page menu, or from the page's **More actions** menu in the sidebar. 2. Confirm with **Delete**. The page moves to the recycle bin and you go back to the space. A message offers **Undo** if you change your mind straight away. Otherwise you can restore it from the recycle bin within 30 days. See [Recycle bin](/spaces/recycle-bin). Anyone who had the page open is taken off it. --- # Formatting text Format text in the Folioso editor with the toolbar, keyboard shortcuts or Markdown-style typing. This page covers text styles, headings, lists, quotes, links, alignment and tables. ## The toolbar The formatting toolbar sits above the page whenever you can edit it. On a phone it's a single row you can scroll sideways. ![The editor with its formatting toolbar](/images/docs/screens/editor-light.webp) | Button | What it does | | --- | --- | | **Bold**, **Italic**, **Strikethrough**, **Inline code** | Style the selected text | | **Link** | Add, change or remove a link. See [Links](#links). | | **Align left**, **Align centre**, **Align right** | Align the paragraph, heading or image | | **Heading 1**, **Heading 2**, **Heading 3** | Turn the line into a heading | | **Bullet list**, **Ordered list**, **Task list** | Start a list | | **Blockquote** | Turn the paragraph into a quote | | **Code block** | Insert a code block. See [Code blocks](/editor/code-blocks). | | **Diagram** | Insert a diagram. See [Diagrams](/editor/diagrams). | | **Insert table** | Insert a 3×3 table with a header row | | **Insert image** | Upload one or more images. Shown when your plan allows image uploads. See [Images](/editor/images). | | **Alert box** | Insert an alert box. See [Alert boxes](/editor/alerts). | | **Horizontal rule** | Insert a dividing line | Buttons that can't apply where your cursor is are greyed out. For example, inside an alert box only the text styles and **Link** work, and the alignment buttons are off inside a code block. The toolbar is one stop when you press Tab. Use the left and right arrow keys, Home and End to move between its buttons. ## Text styles Select text and click a toolbar button, use a shortcut, or type the Markdown markers around a word. | Style | Shortcut | Type | | --- | --- | --- | | Bold | Ctrl B | `**text**` or `__text__` | | Italic | Ctrl I | `*text*` or `_text_` | | Strikethrough | Ctrl Shift S | `~~text~~` | | Inline code | Ctrl E | `` `text` `` | | Underline | Ctrl U | — | On a Mac, use ⌘ instead of Ctrl. Underline has no toolbar button, and it isn't kept when you export to Markdown. ## Headings Use **Heading 1**, **Heading 2** or **Heading 3** on the toolbar, or type one to three `#` characters and a space at the start of a line: `# `, `## ` or `### `. The keyboard shortcut is Ctrl Alt and the heading level, for example Ctrl Alt 2. Ctrl Alt 0 turns a heading back into a paragraph. Once a page has two or more headings, an **On this page** outline appears beside the text on a wide screen. Click a heading in it to jump there. On a narrower screen it's under **Outline, child pages and backlinks** at the end of the page. ## Lists | List | Toolbar | Type at the start of a line | Shortcut | | --- | --- | --- | --- | | Bullet list | **Bullet list** | `- `, `* ` or `+ ` | Ctrl Shift 8 | | Numbered list | **Ordered list** | `1. ` | Ctrl Shift 7 | | Task list | **Task list** | `[ ] ` or `[x] ` | Ctrl Shift 9 | - Press **Tab** to indent a list item under the one above, and **Shift Tab** to move it back out. - Press Enter on an empty item to end the list. - Tick a task item's checkbox to mark it done. ## Quotes and dividers - **Blockquote:** click **Blockquote**, press Ctrl Shift B, or type `> ` at the start of a line. - **Horizontal rule:** click **Horizontal rule**, or type `---` at the start of a line. - **Line break:** press Shift Enter for a new line inside the same paragraph. ## Links ### Link to a web page 1. Select the text you want to link, or put your cursor where the link should go. 2. Click **Link** on the toolbar. 3. Type or paste the address and press Enter, or click **Apply link**. If you typed an address without `https://`, Folioso adds it. With nothing selected, the address itself is inserted as linked text. To change a link, put your cursor in it and click **Link** again. To remove it, click **Remove link**, or clear the address and apply. Press Escape to close the box without changing anything. Folioso refuses addresses that could run code, such as `javascript:` links. ### Link to another page Type `#` to pick a Folioso page to link to. See [Page links and mentions](/editor/links-and-mentions). ## Alignment Align a paragraph, a heading or an image with **Align left**, **Align centre** or **Align right**, or with the shortcuts: | Alignment | Shortcut | | --- | --- | | Left | Ctrl Shift L | | Centre | Ctrl Shift E | | Right | Ctrl Shift R | List items, task items and table cells follow the alignment of the paragraph inside them. Code blocks, diagrams and alert boxes can't be aligned. Alignment isn't kept when you export to Markdown. ## Tables Click **Insert table** on the toolbar, or choose **Table** from the `/` menu, for a 3×3 table with a header row. You can't put a table inside another table. - **Move between cells** with Tab and Shift Tab. Pressing Tab in the last cell adds a new row. - **Resize a column** by dragging its border. - **Row and column options.** Click the handle along a row's left edge or a column's top edge for **Insert row above**, **Insert row below**, **Move row up**, **Move row down** and **Delete row**, or the column versions: **Insert column left**, **Insert column right**, **Move column left**, **Move column right** and **Delete column**. - **Quick insert.** Click a **+** between two rows or columns to insert one there. - **Whole table.** Click the corner handle to select the table. Its options include **Toggle header row**, **Toggle header column** and **Delete table**. When you're [editing together](/editor/collaboration), Folioso holds off adding, moving or deleting rows and columns, and deleting the table, while someone else is working in the same table, and tells you who's there. Typing in cells still works. ## Undo and redo | Action | Shortcut | | --- | --- | | Undo | Ctrl Z | | Redo | Ctrl Shift Z or Ctrl Y | On a page you're editing with others, undo reverses your own changes, not theirs. ## Leaving the editor with the keyboard Inside the page, Tab indents rather than moving focus. To move focus out, press **Escape** and then **Tab**. Further presses of Tab move through any block controls, such as a code block's language picker, and then out of the editor. For every shortcut in one place, see [Keyboard shortcuts](/editor/keyboard-shortcuts). --- # Code blocks Code blocks hold syntax-highlighted code with line numbers, a language picker and a copy button. This page covers inserting them, choosing a language, indenting, and the defaults you can set. ![A page with highlighted Python and Rust code blocks, each with line numbers and a language menu](/images/docs/screens/editor-code-light.webp) ## Insert a code block Any of these works: - Click **Code block** on the toolbar. - Type `/` at the start of a line and choose **Code block**. - Type three backticks, optionally followed by a language, then a space or Enter. For example, type ` ```php ` and press Enter. Three tildes (`~~~`) work too. - Press Ctrl Alt C (⌘ Option C on a Mac). A new block starts in your default language. See [Set your defaults](#set-your-defaults). To use inline code inside a sentence instead, wrap it in single backticks or press Ctrl E. See [Formatting text](/editor/formatting#text-styles). ## Choose the language Pick the language from the menu at the top left of the block. The block is highlighted as soon as you choose. | Languages in the menu | | --- | | Plain text, TypeScript, JavaScript, PHP, Python, Go, Rust, Bash, SQL, HTML, CSS, YAML, JSON, Dockerfile | When you start a block by typing a fence, common short names are understood: | You type | Language | | --- | --- | | `js` | JavaScript | | `ts` | TypeScript | | `py` | Python | | `sh`, `shell`, `zsh`, `shellscript` | Bash | | `yml` | YAML | | `docker` | Dockerfile | You can type a language that isn't in the menu, such as ` ```ruby `. The block keeps it, shows it in the menu by name, and highlights it where Folioso recognises the language. Otherwise it's shown as plain text. The same applies to code you paste from sites such as GitHub that label their code blocks with a language. **Plain text** turns highlighting off. ## Write and indent code Inside a code block: | Key | What it does | | --- | --- | | Tab | Inserts spaces at the cursor. With several lines selected, indents every one. | | Shift Tab | Removes one level of indentation from the current or selected lines. | | Enter | Starts a new line in the block. | | Enter three times at the end | Leaves the block and starts a new paragraph below it. | | Down arrow on the last line | Leaves the block, onto the line below it. | The number of spaces per Tab is your **Tab Size** setting. The `/` block menu, the `#` page-link picker and the `@` mention picker don't open inside code, so you can type `#include` or `@decorator` freely. ## Copy and move a block - **Copy:** hover over the block and click **Copy** at the top right. The button shows a tick when the code is on your clipboard. Readers who can't edit the page can copy too. - **Move:** hover over the block, then press and hold **Drag to move** beside the copy button and drag the block to a new place on the page. ## Set your defaults Your code block defaults are personal settings. They apply to every page you open, and don't change what other people see. 1. Click **Settings** at the bottom of the sidebar. 2. Under **Editor Preferences**, choose: - **Default Code Language:** the language new code blocks start in. **Plain text** means no highlighting. - **Tab Size:** 2 or 4 spaces per Tab. - **Line Numbers:** whether code blocks show line numbers. Until you change them, blocks start as plain text, Tab inserts 4 spaces and line numbers are on. Changes save automatically. See [Preferences](/account/preferences). ## Code blocks elsewhere - **Export as Markdown** writes each code block as a fenced block with its language, ready for a README. - **PDF exports**, shared pages and public links show the same highlighting. - **Search** covers the code in your blocks, so you can find a page by a function or command name. See [Search](/sharing/search). --- # Diagrams Draw flowcharts, sequence diagrams and more by writing Mermaid or PlantUML source, and Folioso renders the picture on the page. This page covers inserting a diagram, editing its source and viewing large diagrams. ![A page with a rendered Mermaid flowchart](/images/docs/screens/editor-diagram-light.webp) ## Insert a diagram - Click **Diagram** on the toolbar, or - Type `/` at the start of a line and choose **Mermaid diagram**. A small sample flowchart appears. Hover over it and click **Edit source** to replace it with your own. ## Mermaid and PlantUML A diagram uses one of two engines. You pick it in the diagram editor, and you can switch at any time. | Engine | Good for | Example source | | --- | --- | --- | | **Mermaid** | Flowcharts, sequence, class and state diagrams, and many more | `graph TD; A[Start] --> B[End];` | | **PlantUML** | UML diagrams: sequence, class, activity, component and state | `@startuml` … `@enduml` | Each diagram editor has a link to the engine's syntax reference: **Mermaid syntax →** or **PlantUML syntax →**. ```text @startuml Alice -> Bob: hello Bob --> Alice: hi @enduml ``` The first PlantUML diagram you open can take a moment to appear while its renderer loads. ## Edit a diagram You need to be able to edit the page. 1. Hover over the diagram and click **Edit source**. 2. In **Edit diagram**, choose **Mermaid** or **PlantUML**. 3. Write the source in the left pane. The preview on the right redraws a moment after you stop typing. 4. Click **Save**, or press Ctrl Enter (⌘ Enter on a Mac). Click **Cancel** or press Escape to close without saving. Use **Split**, **Editor** and **Preview** at the top to show both panes, just the source or just the drawing. On a phone the editor opens on **Editor**. ### When the source has a mistake If the source can't be drawn, the preview shows **Diagram error** with the engine's message above the last version that worked, and **Save** stays off with "Fix the diagram error to save" until you fix it. The diagram on the page doesn't change until you save working source. ### Indenting the source In the source pane, Tab indents and Shift Tab outdents, using your Tab Size setting. To use Tab to move between the editor's buttons instead, press Ctrl M. Press it again to go back to indenting. The hint at the bottom of the editor shows which mode you're in. ## View a large diagram Hover over the diagram and click **Expand** to open it in a viewer. - Scroll with the mouse wheel to zoom in and out, or use **Zoom in** and **Zoom out**. - Drag to move around. - Click **Fit and centre** to see the whole diagram again. The preview in the diagram editor works the same way. ## Move a diagram Drag the diagram to another place on the page. ## Diagrams elsewhere - Diagrams follow your light or dark theme. - **Export as Markdown** writes each diagram as a fenced `mermaid` or `plantuml` block, so tools that render Mermaid, such as GitHub, can draw it. - **Search** finds the text labels in your diagrams, such as box and arrow labels, but not the syntax around them. - When you're [editing together](/editor/collaboration), a diagram someone else is editing is locked for everyone else, with a note saying who's editing it. --- # Alert boxes Alert boxes are titled, coloured panels for the things a reader mustn't miss: a tip, a success, a warning or an error. This page covers inserting them, changing their type and what they can hold. ![A page with error, warning, success and info alert boxes, each with a title](/images/docs/screens/editor-alerts-light.webp) ## The four types | Type | Use it for | | --- | --- | | **Info** | Background, notes and context | | **Success** | Confirmations and things that went right | | **Warning** | Something to be careful about | | **Error** | Something that will break or must not be done | Each type has its own colour and icon. ## Insert an alert box - Type `/` at the start of a line and choose **Info box**, **Success box**, **Warning box** or **Error box**. - Or click **Alert box** on the toolbar, which inserts an **Info** box. If your cursor is on an empty line, the box replaces it. Otherwise it goes in below the current block. The cursor lands in the box's title. ## Write in an alert box A box has two parts: - **Title:** one line. The placeholder reads **Title**. - **Description:** the text underneath. The placeholder reads **Description**. Inside a box: | Key | What it does | | --- | --- | | Enter in the title | Moves to the description. | | Enter in the description | Starts a new line of description. | | Enter twice at the end | Leaves the box and starts a new paragraph below it. | | Tab / Shift Tab | Moves forwards or backwards through the title and the lines of the description, and out of the box. | | Backspace at the start of an empty title | Removes the box, if its description is empty too. | ## Change the type Click inside the box. A row of four icons appears on the box: **Info**, **Success**, **Warning** and **Error**. Click one to switch. The icons are hidden again when your cursor leaves the box. ## What a box can hold Alert boxes are kept simple on purpose: - **Text only.** The description holds paragraphs of text, with bold, italic, strikethrough, inline code and links. Headings, lists, code blocks, tables and images can't go inside, so those toolbar buttons are off while your cursor is in a box. An image you drop or upload while in a box is placed just after it. - **Top level only.** A box can't go inside a list, a quote, a table or another box. **Alert box** is greyed out when your cursor is somewhere a box can't go. - **No alignment.** Text in a box is always left-aligned. To move a box, drag it by its icon. ## Alert boxes elsewhere - **Export as Markdown** writes each box in GitHub's alert format, so it renders as an alert on GitHub: Info as `[!NOTE]`, Success as `[!TIP]`, Warning as `[!WARNING]` and Error as `[!CAUTION]`. The title becomes a bold first line. - **Search** covers the title and description. - When you're [editing together](/editor/collaboration), a box someone else is working in is locked for you, with a note saying who's editing it. --- # Images Add screenshots and pictures to a page by uploading, pasting or dragging them in, then resize, align and caption them in place. This page covers adding images, the size and storage limits, and editing images on the page. ![A page with a chart image and its caption](/images/docs/screens/editor-images-light.webp) ## Who can add images Anyone who can edit the page can add images, on every plan. If image uploads aren't available, the **Insert image** button and the **Image** item in the `/` menu don't appear, and pasted images aren't uploaded. ## Add an image Any of these works: - **Toolbar:** click **Insert image** and choose one or more files. - **Block menu:** type `/` at the start of a line and choose **Image**. - **Paste:** copy an image, for example a screenshot, and paste it into the page. - **Drag and drop:** drag image files from your computer onto the page. They land where you drop them. - **Paste an image address:** with nothing selected, paste the address of an image on the web, such as `https://example.com/diagram.png`. Folioso fetches the image and stores its own copy, so the page doesn't depend on the other site. If the address isn't an image, it's pasted as a link instead. While an image is being added, a progress message shows **Uploading image…**, **Processing image…** and **Saving image…**, or **Fetching image…** for an address. You can add several images at once. ## Formats and limits - **Formats:** PNG, JPEG, GIF, WebP and AVIF. Folioso converts every image to WebP when it stores it, which keeps pages fast. Animated images are stored as a still image. - **Dimensions:** images over 25 megapixels are refused. - **Upload rate:** up to 50 images an hour per person. ### Largest image per plan Your plan sets the largest single image you can upload. Whatever the plan, no single image can be over 25 MB. An image over your limit is refused with "Images must be under … MB on your plan." See the [pricing page](https://folioso.dev/pricing) for each plan's limit. ### Storage Images count towards your organisation's image storage, which everyone in it shares. The allowance depends on your plan and, on a per-seat plan, on the number of seats. When an upload would take the organisation over its allowance, it's refused and the message suggests asking an organisation admin to upgrade. Images already on pages are never removed. See [Uploads](/organisation/uploads#how-storage-is-counted). ## Caption an image Click the image and type in **Add a caption…** below it. Press Enter, or click away, to save. The caption is also the image's alt text, which screen readers announce, so describe what the image shows. Captions can be up to 500 characters, and they're included in search. ## Resize an image - **Drag:** click the image, then drag one of its corner handles. An image can't be narrower than 80 pixels or wider than the page. - **Keyboard:** click the image, press Tab to reach the resize handle, then use the arrow keys to make it 10 pixels wider or narrower, or hold Shift for 50 pixels. - **Full width:** click **Full width** below the image to fill the width of the page. Click it again to undo. ## Align an image Click the image, then use **Align left**, **Align centre** or **Align right** on the toolbar, or the alignment shortcuts in [Formatting text](/editor/formatting#alignment). ## View an image full size Click **Expand** below the image to open it in a viewer. Click **Close** or press Escape to go back. ## Move or remove an image - **Move:** drag the image to another place on the page. - **Remove:** click the image to select it and press Backspace or Delete. ## When an image won't load If the image file can't be loaded, the page shows **Image unavailable** in its place. Ask whoever added it to upload it again. ## Images elsewhere - Images appear on pages you share and on public links, and in PDF exports. - **Export as Markdown** writes each image as a Markdown image pointing at its address in Folioso, with its caption as the alt text. - When you're [editing together](/editor/collaboration), an image someone else is resizing or captioning is locked for you, with a note saying who's editing it. --- # Page links and mentions Link to another Folioso page by typing `#`, and tag a colleague by typing `@`. This page covers both pickers, what readers see, and who gets notified. ## Link to another page 1. In the body of a page you can edit, type `#` at the start of a line or after a space. 2. Keep typing part of the page's title. A list of matching pages appears, each with its space and group underneath. 3. Use the arrow keys to choose and press Enter or Tab, or click a page. Press Escape to close the list. The link appears as a chip with the page's title. Click it to open the page. - **What you can link to:** any page you can open, in any space, and in any organisation you belong to. The list shows up to eight matches, most recently edited first. If nothing matches, it says **No matching pages**. - **Where it works:** not inside code blocks or inline code, so `#include` and shell comments are left alone. A `#` followed straight away by a space still makes a heading, as usual. ### Titles stay up to date A page link stores which page it points to, not its title. Every reader sees the page's current title, so renaming a page updates every link to it. Readers who can't open the linked page see **Restricted page** with a lock instead of the title, so a link never reveals a page they don't have access to. ### Linked from When another page links to the one you're reading, it's listed under **Linked from** beside the text (or in **Outline, child pages and backlinks** at the end of the page on a narrower screen). You only see linking pages you can open yourself. ## Mention someone 1. Type `@` at the start of a line or after a space. 2. Type part of the person's name. Up to eight matching people appear. 3. Choose one with the arrow keys and Enter or Tab, or click them. The mention appears as `@Name` in the text. If nobody matches, the list says **No matching users**. Typing an email address such as `me@example.com` doesn't open the list. ### Who you can mention You can mention anyone who can open the page: - the page's author; - everyone the page has been shared with; - unless the page is private, everyone who has a role in the page's space. ### What the person gets When the page saves, each person you mentioned gets an in-app notification: "*Your name* tagged you in "*Page title*"". It appears under the notifications bell and links straight to the page. Mentioning yourself doesn't notify you. Mentions don't send email. See [Notifications](/sharing/notifications). Mentioning someone doesn't give them access to the page. If you need someone who can't see the page to read it, share it with them. See [Sharing with people](/sharing/sharing-with-people). ## Who can add links and mentions The `#` and `@` pickers only appear for people who can edit the page. ## Links and mentions elsewhere - **Export as Markdown** writes a page link as a normal Markdown link with the page's title and address, or as `Restricted page` if you can't open the page. A mention is written as `@Name`. - **Search** doesn't include the text of page links or mentions. To link to a website rather than a Folioso page, see [Links](/editor/formatting#links). --- # Tags Tags are coloured labels you put on pages, such as `python` or `incident`, so related pages are easy to spot. This page covers adding tags to a page, creating and managing tags, and how tags belong to spaces. ## How tags work - **Tags belong to a space.** Each space has its own set, and a page can only carry tags from its own space. A tag called `api` in one space is a different tag from `api` in another. - **Names are unique within a space.** A space can't have two tags with the same name. - **Every space starts with language tags**, one for each language in the code block menu, such as TypeScript, PHP, Python and SQL, each in its own colour. These default tags can't be renamed, recoloured or deleted. Tags appear under a page's title and on **All Pages**. ## Add a tag to a page You need the Editor or Admin role in the space, or to be the page's author. 1. Open the page. Under the title, click **Add tag**. 2. Start typing in **Search or create tag…**. Matching tags from the space appear. 3. Click a tag, or use the arrow keys and Enter, to add it. To create a new tag on the spot, type its name, choose a **Colour**, and pick **Create "*name*"** at the end of the list. It's added to the space and to the page. To take a tag off the page, click the **×** on it (**Remove tag *name***). The tag stays in the space for other pages. People who can only view the page see its tags but can't change them. ## Manage a space's tags Tags for the space you're in are managed from your settings. 1. Click **Settings** at the bottom of the sidebar. 2. Find **Tags in *space name***. From there you can: | Task | How | Who | | --- | --- | --- | | Find a tag | Type in **Search tags…** | Anyone in the space | | Create a tag | Type in **New tag name…**, pick a colour and click **Add** | Editors and admins | | Rename a tag | Click **Rename**, change the name and click **Save** | Editors and admins | | Change a tag's colour | Click the colour dot and pick a new one | Editors and admins | | Delete a tag | Click **Delete**, check the pages listed under "It is currently used on:", then click **Delete** | Space admins only | Tag names can be up to 50 characters. If the name is taken, you'll see "This space already has a tag with that name." Deleting a tag removes it from every page that carries it. The pages themselves aren't changed otherwise. To manage another space's tags, switch to that space first, then open Settings. ## Tags when pages move - **Moving a page to another space** takes its tags along. Each tag is matched to the tag with the same name in the new space, which is created if it doesn't exist yet. - **Duplicating a page** copies its tags. - **Deleting a space** moves its pages, with their tags, into the default space. --- # Editing together When several people open the same page, Folioso keeps everyone's screen in sync as they type, with live cursors and faces in the header. This page explains when live editing switches on, what you'll see, and how conflicts are avoided. ## When live editing is on Live editing is part of every plan. It switches on automatically for a page that other people can reach: - the page is shared with someone, by email or with a public link, or - the page isn't private and its space has more than one member. A page only you can reach, such as a page in My space that you haven't shared, opens as a normal single-person page. When a live page opens, you may briefly see **Connecting to live session…** while Folioso joins it. ## What you'll see - **Who's here.** The header shows a face for everyone else on the page. Hover over one to see their name and whether they're editing or viewing. - **Live cursors.** Each editor's cursor and selection appear in the text in their own colour, labelled with their name and picture. - **Changes as they happen.** Text, formatting, code blocks, diagrams and every other part of the page update for everyone as they're typed. The title updates too. - **Who edited last.** The line under the title shows who last changed the page. People who can only view the page see the changes and cursors live, but can't type. ## Saving You don't need to do anything. While you're editing together, the page saves about two and a half seconds after the last change, and one person's editor saves on everyone's behalf. The header shows **Saving…** and **Saved** as usual. Ctrl S (⌘S on a Mac) saves straight away. ## Avoiding clashes Two people can type in the same paragraph at once, and Folioso merges their changes. A few things are held back so that one person can't undo another's work by accident: - **Images, diagrams and alert boxes.** While someone is working on one, such as resizing an image, editing a diagram's source or typing in an alert box, it's locked for everyone else. Hover over it to see who's editing, for example "Sam is editing this diagram". - **Tables.** While someone else is in a table, you can still type in its cells, but you can't add, move or delete rows and columns, or delete the table. A message tells you who's in it. These locks are released as soon as the other person moves on. ## Undo Ctrl Z (⌘Z on a Mac) undoes your own recent changes, not other people's. ## When access changes If your access to the page changes while you have it open, for example an owner turns your edit access into view-only, your editor updates to match. If you lose edit access with changes that couldn't be saved, a **Your access changed** dialog explains what happened. Copy anything you need from the screen, then click **Reload page**. If the page is deleted while you're on it, you're taken off it. ## When a snapshot is restored If someone restores an earlier version of the page from its [version history](/editor/version-history), everyone's editor reloads the restored version, with the message "This page's content was replaced, so it has been reloaded." Changes that weren't saved before the restore aren't kept, so it's worth checking with your colleagues before restoring a busy page. The same happens when an integration replaces the page's content through the [API](/api/pages). --- # Version history Version history keeps snapshots of a page, taken every night it changes and whenever you take one by hand, so you can look back at an earlier version and restore it. This page covers taking, viewing and restoring snapshots, how long they're kept and who can see them. ## Plans Version history is available if your plan includes it. If it doesn't, **Snapshots** in the page menu carries a **Pro** badge and opens an upgrade prompt. See the [pricing page](https://folioso.dev/pricing). ## Kinds of snapshot | Kind | Label in the list | Taken | | --- | --- | --- | | **Daily** | Daily, by "Automatic" | Every night, for each page whose content changed since its last snapshot | | **Manual** | Manual, by the person who took it | Whenever someone clicks **Take snapshot** | | **Before restore** | Before restore, by the person who restored | Automatically, just before a snapshot is restored | A snapshot holds the page's title and content as they were last saved. It doesn't include tags, sharing or the page's location. Daily snapshots run overnight (UTC) and only for pages edited in the previous two days whose content is different from their latest snapshot, so an unchanged page doesn't pile up copies. There are no daily snapshots from before your organisation had version history. ## Open the snapshots 1. Open the page. 2. Click **More actions** in the header and choose **Snapshots**. The **Snapshots** panel lists the page's snapshots, newest first. Each one shows its label (or its kind), who took it, when, and its word count. ![The Snapshots panel, with the label field and Take snapshot above a manual and a daily snapshot](/images/docs/screens/version-history-light.webp) ## Take a snapshot Take a manual snapshot before a big rewrite, so you have a known-good version to come back to. 1. Open **Snapshots**. 2. Optionally type a **Label (optional)**, such as "Before the refactor". Labels can be up to 80 characters. 3. Click **Take snapshot**. Folioso saves your recent edits first where it can, then captures the page. You'll see "Snapshot taken." You can take up to 10 snapshots a minute. ## View a snapshot Click a snapshot in the list to open it. You see the page exactly as it was, read-only, with its kind, label, date and author at the top. ![A snapshot open read-only under a You're viewing a snapshot bar, with Back to current page and Restore this snapshot](/images/docs/screens/snapshot-light.webp) Click **Back to current page** to return to the editor. ## Restore a snapshot 1. Open the snapshot. 2. Click **Restore this snapshot**. 3. Confirm with **Restore**. What happens: - Folioso first takes a **Before restore** snapshot of the page as it is now, so you can undo the restore by restoring that one. - The page's title and content are replaced with the snapshot's, for everyone. Anyone with the page open is reloaded onto the restored version. See [Editing together](/editor/collaboration#when-a-snapshot-is-restored). - The restore shows in the space's activity as an edit by you. - People mentioned in the restored content aren't notified again. ## How long snapshots are kept Manual and **Before restore** snapshots are kept for as long as the page exists, whatever your plan. Daily snapshots are kept for a number of days set by your plan, and some plans keep them indefinitely. See the [pricing page](https://folioso.dev/pricing). A page always keeps its most recent snapshot, even when it's older than that. Snapshots are deleted with the page when it's permanently removed from the recycle bin. If your organisation moves to a plan without version history, no new daily snapshots are taken and the **Snapshots** panel is unavailable until you upgrade again. Existing daily snapshots older than that plan's retention period are deleted, apart from each page's most recent one. Manual and **Before restore** snapshots are kept. ## Who can see snapshots Snapshots are available to people with the Editor or Admin role in the page's space, and to the page's author. People who can open the page only because it was shared with them, or through a public link, can't see its history. To keep earlier drafts away from a new audience, Folioso hides older snapshots whenever a page's audience changes: - the page is moved to another space; - the page's space is deleted and its pages move to the default space; - the space becomes open; - a private page is made visible to its space. Snapshots from before the change are hidden from everyone, the author included, and can't be viewed or restored. Snapshots taken after the change work as normal. --- # Exporting pages Export a page as a PDF to send or print, or as a Markdown file to use in a repository or another tool. This page covers both, who can export, and what each format includes. ## Where to find export - **In the editor,** click **More actions** in the page header and choose **Export as PDF** or **Export as Markdown**. This menu is for people with the Editor or Admin role in the space and for the page's author. - **On All Pages,** open a page's **Page actions** menu. Anyone who can see the page there can export it, viewers included. People who can open a page only because it was shared with them don't get either menu. To export everything you've written at once, use the data export in your settings instead. See [Exporting your data](/account/exporting-your-data). ## Export as PDF PDF export is available if your plan includes it. If it doesn't, **Export as PDF** carries a **Pro** badge and opens an upgrade prompt. See the [pricing page](https://folioso.dev/pricing). 1. Choose **Export as PDF**. 2. Folioso builds the PDF. The dialog shows "Building your PDF of …", which usually takes a few seconds. You can keep working while you wait. 3. When it says **Your PDF is ready**, click **Download PDF**. The PDF has the page's formatting, highlighted code, diagrams and images. ### If you close the dialog If you close the dialog before downloading, Folioso emails you a link a few minutes after the PDF is ready, with the subject 'Your PDF of "…" is ready'. You'll only get the email if you haven't already downloaded the file. - The download link only works when you're signed in to your own account. It isn't a shareable link. - The file is kept for **7 days**. After that, export the page again. - You must still be able to open the page when you download. If the page is deleted or your access is removed in the meantime, the download stops working. If the export takes longer than usual, the dialog says "This is taking longer than expected." and you can close it and wait for the email. ### Limits You can start up to **4 PDF exports an hour**. After that the dialog says "You have reached the export limit for this hour. Try again later." ## Export as Markdown Markdown export is available on every plan. Choose **Export as Markdown** and your browser downloads a `.md` file straight away, named after the page's title. The file starts with the page's title as a heading, followed by the content: | In the page | In the Markdown | | --- | --- | | Bold, italic, strikethrough, inline code, links | The usual Markdown markers | | Headings, lists, task lists, quotes, dividers | Markdown headings, lists, `- [ ]` tasks, `>` quotes and `---` | | Tables | Markdown tables | | Code blocks | Fenced code blocks with their language | | Diagrams | Fenced `mermaid` or `plantuml` blocks with the diagram's source | | Alert boxes | GitHub alerts: `[!NOTE]`, `[!TIP]`, `[!WARNING]` or `[!CAUTION]` | | Images | Markdown images that point at the image's address, with the caption as alt text | | Page links | Links with the page's title and address, or `Restricted page` if you can't open it | | Mentions | `@Name` | Underline and text alignment have no Markdown equivalent, so they're left out. Images aren't copied into the file: it links to them where they're stored in Folioso. The export includes everything on your screen, even changes that haven't saved yet. ```markdown # Deploy checklist > [!WARNING] > **Freeze first** > Announce the freeze in #releases before you start. - [ ] Run the migrations - [x] Tag the release ``` --- # Keyboard shortcuts Every keyboard shortcut in Folioso, grouped by where you use it. On a Mac, use ⌘ (Command) where the tables say Ctrl, and Option where they say Alt. ## Anywhere in the app | Shortcut | What it does | | --- | --- | | Ctrl K | Open the command palette to search pages. Pressing it again while the palette is open selects your search. | ### In the command palette | Shortcut | What it does | | --- | --- | | Up / Down arrow | Move through the results | | Enter | Open the highlighted page | | Escape | Close the palette | See [Search](/sharing/search). ## Saving and history | Shortcut | What it does | | --- | --- | | Ctrl S | Save the page now, instead of waiting for autosave | | Ctrl Z | Undo | | Ctrl Shift Z or Ctrl Y | Redo | On a page you're editing with others, undo and redo apply to your own changes only. ## Text styles | Shortcut | What it does | | --- | --- | | Ctrl B | Bold | | Ctrl I | Italic | | Ctrl U | Underline | | Ctrl Shift S | Strikethrough | | Ctrl E | Inline code | ## Blocks | Shortcut | What it does | | --- | --- | | Ctrl Alt 1 to Ctrl Alt 6 | Heading level 1 to 6 | | Ctrl Alt 0 | Turn a heading back into a paragraph | | Ctrl Shift 8 | Bullet list | | Ctrl Shift 7 | Numbered list | | Ctrl Shift 9 | Task list | | Ctrl Shift B | Blockquote | | Ctrl Alt C | Code block | | Shift Enter or Ctrl Enter | Line break inside the same paragraph | ## Alignment | Shortcut | What it does | | --- | --- | | Ctrl Shift L | Align left | | Ctrl Shift E | Align centre | | Ctrl Shift R | Align right | These work on paragraphs, headings and images. Inside a code block, diagram or alert box they do nothing. ## Markdown-style typing Type these at the start of a line, followed by a space (or Enter for a code block): | Type | You get | | --- | --- | | `#`, `##`, `###` … | Heading 1, 2, 3 and so on | | `-`, `*` or `+` | Bullet list | | `1.` | Numbered list | | `[ ]` or `[x]` | Task list, unticked or ticked | | `>` | Blockquote | | ` ``` ` or `~~~`, optionally with a language | Code block. See [Code blocks](/editor/code-blocks). | | `---` | Horizontal rule (no space needed) | And around text, anywhere in a line: | Type | You get | | --- | --- | | `**text**` or `__text__` | Bold | | `*text*` or `_text_` | Italic | | `~~text~~` | Strikethrough | | `` `text` `` | Inline code | ## Pickers and menus | Type | What it opens | | --- | --- | | `/` at the start of a line | The block menu | | `#` at the start of a line or after a space | The page-link picker | | `@` at the start of a line or after a space | The mention picker | In any of them: | Shortcut | What it does | | --- | --- | | Up / Down arrow | Move through the list | | Enter or Tab | Insert the highlighted item | | Escape | Close the list | See [Creating and managing pages](/editor/pages#the--block-menu) and [Page links and mentions](/editor/links-and-mentions). ## The page title | Shortcut | What it does | | --- | --- | | Enter | Move from the title to the start of the page body | ## Lists | Shortcut | What it does | | --- | --- | | Tab | Indent the list item under the one above | | Shift Tab | Move the list item back out a level | | Enter on an empty item | End the list | ## Code blocks | Shortcut | What it does | | --- | --- | | Tab | Insert spaces, or indent every selected line | | Shift Tab | Remove one level of indentation from the current or selected lines | | Enter three times at the end | Leave the code block | | Down arrow on the last line | Leave the code block | The number of spaces is your Tab Size setting. See [Preferences](/account/preferences). ## Tables | Shortcut | What it does | | --- | --- | | Tab | Move to the next cell. In the last cell, add a new row. | | Shift Tab | Move to the previous cell | ## Alert boxes | Shortcut | What it does | | --- | --- | | Enter in the title | Move to the description | | Enter twice at the end of the description | Leave the box | | Tab / Shift Tab | Move forwards or backwards through the title and description lines, and out of the box | | Backspace at the start of an empty title | Remove the box, if its description is empty too | ## Images Click an image to select it, then: | Shortcut | What it does | | --- | --- | | Tab | Move to the image's resize handle | | Right or Up arrow | Make the image 10 pixels wider (on the resize handle) | | Left or Down arrow | Make the image 10 pixels narrower (on the resize handle) | | Shift and an arrow | Resize by 50 pixels instead (on the resize handle) | | Enter | Save the caption (in **Add a caption…**) | | Backspace or Delete | Remove the image | ## Diagram editor In **Edit diagram**: | Shortcut | What it does | | --- | --- | | Ctrl Enter | Save the diagram | | Tab / Shift Tab | Indent or outdent the source | | Ctrl M | Switch Tab between indenting and moving between buttons. This one is Ctrl on a Mac too. | | Escape | Close without saving | ## Links In the toolbar's **Link** box: | Shortcut | What it does | | --- | --- | | Enter | Apply the link | | Escape | Close the box without changes | ## Sidebar When renaming a page in the sidebar: | Shortcut | What it does | | --- | --- | | Enter | Save the new name | | Escape | Cancel | ## Moving focus out of the editor Inside the page body, Tab indents instead of moving focus. To leave the editor with the keyboard: | Shortcut | What it does | | --- | --- | | Escape, then Tab | Move focus out of the text. Further Tabs move through any block controls, such as a code block's language picker, and then out of the editor. | | Escape, then Shift Tab | Move focus backwards out of the text | The formatting toolbar is a single Tab stop. Once you're on it, use the Left and Right arrow keys, Home and End to move between its buttons. --- # About spaces Spaces are the main way to organise pages and control who can see them in Folioso. Each space holds its own groups, pages and tags, and has an access type that decides who can get in. ## What a space is A space belongs to one organisation. Everything you write lives in a space: - **Groups** are flat folders that sort the pages in a space. See [Groups](/spaces/groups). - **Pages** are your notes, code and diagrams. See [Pages](/editor/pages). - **Tags** are scoped to the space. The same tag name in two spaces is two separate tags. See [Tags](/editor/tags). A space has its own address under your organisation's address: a space named "Engineering" lives at `/engineering`. You switch spaces from the organisation and space switcher at the top of the sidebar. It lists **My space** first, then the organisation's other spaces under **Spaces**. ![A space's All Pages home, with its groups in the sidebar and its pages listed](/images/docs/screens/space-home-light.webp) ## Access types Every shared space has one of three access types. | Access type | Who can find it | Who can get in | | --- | --- | --- | | **Open** | Every member of the organisation | Every member, as an editor | | **Closed** | Listed in **Browse spaces** for every member | Only the people and teams granted a role. Anyone else can ask for access. | | **Private** | Only the people and teams granted a role | Only the people and teams granted a role | Organisation admins are admins of every open, closed and private space, so they can always get in. - **Open** suits shared team knowledge. Every member can read and edit pages, and no grants are needed. - **Closed** suits a project or client space that people should know exists but join on purpose. A member without access sees a "restricted content" screen with a **Request access** button. See [Requesting access](/spaces/access-requests). - **Private** suits sensitive work. The space doesn't appear in the directory, and members without a grant can't open it. > [!NOTE] > Private spaces are available only if your plan includes them. See the [pricing page](https://folioso.dev/pricing). If your organisation moves to a plan without them, existing private spaces stay private. You just can't make another space private until you upgrade. To change a space's access type, see [Changing the access type](/spaces/creating-and-managing#changing-the-access-type). ## The default space Every organisation has exactly one default space, created with the organisation and named **General**. It's marked **Default** in the sidebar and the spaces lists. - It's always open, so every member has somewhere to land. You can't make it closed or private. - You can't delete it. When another space is deleted, its pages and groups move to the default space. - You can rename it. ## My space Every member gets one personal space in each organisation they belong to, shown as **My space** and drawn with their own avatar. - It's private to you. Nobody else holds a role in it, **including organisation admins**. - You can't add people or teams to it, change its access type, give it a logo or delete it. - You can still share individual pages from it, by email or by public link. See [Sharing with people](/sharing/sharing-with-people). - My space doesn't count towards your plan's space limit. If you leave the organisation, an empty My space is removed. A My space that still has pages stays behind, and organisation admins can deal with it from [Former members' content](/organisation/former-members-content). ## Space limits by plan Your plan sets how many shared spaces the organisation can have. The default space counts towards the limit. My space doesn't. Some plans have no limit, and whether you can make a space private also depends on your plan. Closed spaces are available on every plan. When you reach the limit, **New space** shows **Upgrade** and takes organisation admins to the Billing page. See the [pricing page](https://folioso.dev/pricing) for each plan's limits, and [Plans](/billing/plans) for how plans work. ## Roles in a space People in a space hold one of three roles: **Viewer**, **Editor** or **Admin**. A role can come from a direct grant, from a team, from the space being open or from being an organisation admin. See [Roles and permissions](/spaces/permissions) for how roles combine and what each one can do. --- # Creating and managing spaces Organisation admins create, rename and delete spaces. Space admins and organisation admins manage a space's access type, logo, people and teams from the **Spaces** tab of the organisation hub. ## Create a space Only organisation admins can create spaces. 1. Open the organisation and space switcher at the top of the sidebar. 2. Choose **New space**. You can also use **New space** on the **Spaces** tab of the organisation hub. 3. Enter a **Name** of up to 120 characters. 4. Under **Who can get in**, pick **Open**, **Closed** or **Private**. The hint below the control explains each one. 5. Choose **Create space**. Folioso opens the new space straight away. You can add people and teams to it from the **Spaces** tab. > [!NOTE] > **Private** is available only if your plan includes private spaces. Otherwise the option is disabled and the dialog names the plans that include it. See the [pricing page](https://folioso.dev/pricing). If your organisation has reached its plan's space limit, **New space** shows **Upgrade** and takes you to the Billing page instead. See [Space limits by plan](/spaces/overview#space-limits-by-plan). ## The space's address A space's address is made from its name when you create it: "Client Work" becomes `/client-work`. If the name clashes with another space in the organisation, or with a word Folioso uses for its own pages (such as "settings" or "search"), a short random suffix is added, for example `/settings-4fk2`. The address doesn't change when you rename the space, so links and bookmarks keep working. You can't edit the address from the app. ## Rename a space Organisation admins can rename any shared space, including the default space. 1. Open the organisation and space switcher at the top of the sidebar. 2. Select **More actions** next to the space, or right-click it. 3. Choose **Rename**, type the new name and press Enter. Press Escape to cancel. My space can't be renamed. ## Manage a space from the Spaces tab The **Spaces** tab of the organisation hub lists the spaces you can manage: - Organisation admins see every shared space. - Space admins see the spaces they're an admin of. This is the one hub tab a member who isn't an organisation admin can open. Open it from **Spaces** in the switcher's admin section. Each space has a card with its logo, a **Default** badge where it applies, its access type, how many people have access, and an **Open** link. ![A closed space's card on the Spaces tab, with its access type, a pending access request, and the people and teams who have a role](/images/docs/screens/space-settings-light.webp) From each card you can: - change the access type; - upload or remove the space's logo; - approve or decline access requests (see [Requesting access](/spaces/access-requests)); - add people and teams, change their roles and remove them (see [Roles and permissions](/spaces/permissions)). ## Changing the access type On the space's card, use the **Access** control to switch between **Open**, **Closed** and **Private**. The change applies immediately. | Change | What happens | | --- | --- | | Open to closed or private | Members without a grant lose access straight away. Anyone with one of the space's pages open is refreshed so the new access takes effect. | | Closed to private | The space disappears from **Browse spaces**. | | Private to closed | The space appears in **Browse spaces**, and members can request access. | | Anything to open | Every member becomes an editor. Older version history is hidden (see below). | | Closed to open or private | Pending access requests are dropped, because they can no longer be answered. | People and team grants are kept when a space becomes open, even though they have no effect while it's open. They apply again if you close the space. > [!IMPORTANT] > When a space becomes open, the version history of its pages from before the change is hidden from everyone, including the authors. Snapshots taken while fewer people could see the page aren't shown to the wider audience. See [Version history](/editor/version-history). You can't change the access type of the default space (always open) or of My space (always private). Making a space private needs a plan that includes private spaces. ## Add a logo A space can have a logo, which appears in the sidebar, the space switcher and the spaces directory. 1. On the space's card on the **Spaces** tab, choose **Upload logo**. 2. Pick a PNG, JPEG, GIF, WebP or AVIF image. The maximum size is set by your plan's upload limit. Use **Change logo** to replace it or **Remove** to go back to the default icon. My space can't have a logo, because it shows its owner's avatar. ## Delete a space Organisation admins can delete any shared space except the default space. 1. Open the organisation and space switcher at the top of the sidebar. 2. Select **More actions** next to the space, or right-click it. 3. Choose **Delete**, then confirm with **Delete**. Deleting a space doesn't delete its content: - **Pages and groups** move to the default space, including pages in the recycle bin. Private pages stay private. Bookmarks that include the deleted space's address stop working, but public links keep working. - **Tags** are merged into the default space by name. A tag that already exists there is reused, and the pages keep their tags. - **People and team grants** on the deleted space are removed. - **Version history** from before the move is hidden, because the default space is open to every member. You can't delete the default space or My space. ## Plan downgrades If your organisation moves to a plan with fewer spaces, existing spaces stay as they are. You can't create new ones until you're under the limit. Private spaces stay private, but you can't make another space private on a plan without the feature. See [Downgrading](/billing/downgrading). --- # Groups Groups are flat folders that sort the pages inside a space. Each group has a name, a colour and an optional icon and description, and appears as a collapsible section in the sidebar. ## How groups work - A group belongs to one space. Each space has its own groups. - Groups are flat: a group can't contain another group. - A page is in one group at most. Pages that aren't in any group appear under **Ungrouped** at the bottom of the sidebar's group list. - Who can see a group follows the space. Anyone who can open the space sees its groups. ## Create a group You need the Editor or Admin role in the space. 1. Open the organisation and space switcher at the top of the sidebar. 2. Choose **New group**. 3. Enter a **Name** of up to 100 characters. 4. Optionally add a **Description** of up to 255 characters. 5. Pick a **Colour** and, optionally, an **Icon**. 6. Choose **Create group**. The group is created in the space you're in. If you only have the Viewer role, **New group** is disabled and says "You have view-only access to this space". ## Group limits by plan Your plan sets how many groups each space can have, and some plans have no limit. At the limit, **New group** shows **Upgrade**. Organisation admins are taken to the Billing page. Other members are told that an organisation admin can upgrade the plan. See the [pricing page](https://folioso.dev/pricing) for each plan's limit. ## Add pages to a group - Open the group's **More actions** menu in the sidebar and choose **Add page** to create a new page in it. - Choose **Move to group…** from a page's actions menu, in the editor or on the **All Pages** list. - Drag a page in the sidebar onto a group's heading or into its list. - When you move a page with **Move to space…**, pick a **Group** in the target space, or **No group**. Moving pages between groups needs the Editor or Admin role, or authorship of the page. See [Pages](/editor/pages). ## Edit a group You need the Editor or Admin role in the space. 1. In the sidebar, open the group's **More actions** menu, or right-click the group. 2. Choose **Edit**. 3. Change the name, description, colour or icon, then choose **Save**. ![The Edit Group dialog, with Name, Description (optional), Colour and Icon (optional)](/images/docs/screens/space-groups-light.webp) ## Delete a group Only space admins, including organisation admins, can delete a group. 1. In the sidebar, open the group's **More actions** menu, or right-click the group. 2. Choose **Delete**, then confirm with **Delete**. The group is deleted permanently, and its pages become **Ungrouped**. No pages are deleted. When a whole space is deleted, its groups move to the default space with their pages. See [Delete a space](/spaces/creating-and-managing#delete-a-space). --- # Roles and permissions Your role in a space decides what you can do with its pages and groups. There are three roles, Viewer, Editor and Admin, and a page can also be made private so that only its author and the people it's shared with can open it. ## The three space roles | Role | What it lets you do in the space | | --- | --- | | **Viewer** | Open the space and read its pages and groups. | | **Editor** | Everything a viewer can do, plus create and edit pages, create and edit groups, and share pages. | | **Admin** | Everything an editor can do, plus move anyone's pages to the recycle bin, restore or delete anyone's deleted pages, make pages private, delete groups, and manage the space's access type, logo, people and teams. | Being a page's **author** also counts. You can always edit, share, move and delete your own pages, even if your role in the space is Viewer. ## Where your role comes from Folioso works out your role in a space in this order. The first rule that applies wins. 1. **My space**: you're the admin of your own My space. Nobody else has a role in it, not even organisation admins. 2. **Organisation admins** are admins of every other space in the organisation: open, closed and private. 3. **Open spaces** make every member of the organisation an editor. 4. **Closed and private spaces** give you the highest role from your direct grant and the grants of every team you're in. If you have none, you have no access. Some consequences worth knowing: - In an **open** space, grants have no effect. Every member is an editor and only organisation admins are admins. Grants are kept and apply again if the space is closed. - Roles add up to the **highest**, never the lowest. If you're granted Viewer directly but your team has Editor, you're an editor. - A member who is [suspended](/organisation/suspending-members) has no role anywhere in that organisation, including their own My space. - People outside the organisation never hold a space role. They can only open pages that have been shared with them. See [Sharing with people](/sharing/sharing-with-people). ## Grant people and teams a role Space admins and organisation admins manage roles from the **Spaces** tab of the organisation hub. ![The People and Teams sections of a space's card on the Spaces tab, each grant with its role and a remove button](/images/docs/screens/space-members-light.webp) 1. Open **Spaces** from the admin section of the organisation and space switcher. 2. Find the space's card. 3. Under **People**, pick a member from **Add a person…**, choose a role, then choose **Add**. 4. Under **Teams**, pick a team from **Add a team…**, choose a role, then choose **Add**. To change a role, pick a new one from the select next to the person or team. To remove a grant, choose **Remove** (the ✕ button) next to them. Removing a grant takes effect straight away, including for anyone who has one of the space's pages open. Only members of the organisation can be added. Teams are created on the [Teams](/organisation/teams) tab. Organisation admins see email addresses in the person picker. Space admins see names only. ## Private pages New pages are visible to everyone with a role in their space. The author or a space admin can make a page **private** instead: 1. Open the page and choose **Share**. 2. In the switch at the top of the dialog, choose **Private**. Choose **Space** to make it visible to the space again. A private page can be opened only by its author and by the people it's been shared with. Space roles don't count on a private page, so space editors, space admins and organisation admins can't open it. The one exception is [Former members' content](/organisation/former-members-content), where organisation admins can reach private pages left behind by people who have left. > [!NOTE] > Making a private page visible to the space hides its version history from before the change, so the wider audience doesn't see drafts written while it was private. See [Version history](/editor/version-history). ## What each person can do with a page This table sums up who can do what with a page that isn't private. On a private page, only the author and people it's shared with have any access. | Action | Viewer | Editor | Page author | Space admin | Shared with you, or opened from a public link | | --- | --- | --- | --- | --- | --- | | Open and read the page | Yes | Yes | Yes | Yes | Yes | | Edit the content | No | Yes | Yes | Yes | With Editor access or an edit link | | Rename, move, duplicate or tag it | No | Yes | Yes | Yes | No | | Share it by email or public link | No | Yes | Yes | Yes | No | | Make it private, or visible to the space | No | No | Yes | Yes | No | | Move it to the recycle bin | No | No | Yes | Yes | No | | Restore it, or delete it forever | No | Pages they deleted | Pages they deleted | Anyone's | No | "Space admin" includes organisation admins, who are admins of every space except other people's My space. In an open space every member is an editor, so a member can edit and share any page, but can only move their own pages to the recycle bin. The app only shows the controls you're allowed to use. If you can't see **Delete** or the private option, your role doesn't include it. ## Space-level abilities | Ability | Viewer | Editor | Admin | | --- | --- | --- | --- | | See the space and its groups | Yes | Yes | Yes | | Create pages | No | Yes | Yes | | Create and edit groups | No | Yes | Yes | | Delete groups | No | No | Yes | | Approve and decline access requests | No | No | Yes | | Change the access type and logo | No | No | Yes | | Add, change and remove people and teams | No | No | Yes | Creating, renaming and deleting spaces is done by organisation admins. See [Creating and managing spaces](/spaces/creating-and-managing). --- # Requesting access Members can ask to join a closed space, and the space's admins approve or decline the request. This page covers both sides: asking for access and answering requests. ## Which spaces you can ask for You can request access only to a **closed** space in an organisation you're a member of. - **Open** spaces don't need a request: every member is already an editor. - **Private** spaces can't be requested. They don't appear in the directory, and only their admins can add people. - **My space** belongs to its owner alone. You can't ask for a space you already have a role in, and you can have one pending request per space. ## Request access There are two ways in. **From the directory:** 1. Open the organisation and space switcher at the top of the sidebar and choose **Browse spaces**. 2. The directory lists every open and closed space in the organisation. Spaces you can already open show **Go to space**. 3. Next to a closed space, choose **Request access**. ![The Spaces directory, listing the organisation's open and closed spaces](/images/docs/screens/spaces-directory-light.webp) **From the space itself:** if you follow a link into a closed space you can't open, you see a "You've stumbled across restricted content" screen. Choose **Request access**. ![The restricted screen of a closed space, with a Request access button](/images/docs/screens/space-restricted-light.webp) Every admin of the space is notified in the app and by email. That includes organisation admins and anyone with the Admin role on the space, directly or through a team. ## Withdraw a request While a request is pending, the directory shows **Requested** next to the space. 1. Open **Browse spaces**. 2. Choose **Withdraw** next to the space. Nobody is notified when you withdraw. ## Approve or decline a request You need to be an admin of the space. Pending requests appear on the **Spaces** tab of the organisation hub, under **Requests** on the space's card. The notification links straight to the request and highlights it. **To approve:** 1. Pick the role to give the person. It starts at **Editor**, and you can choose **Viewer** or **Admin** instead. 2. Choose **Approve**. The person gets the role straight away and an in-app notification saying "You now have access". **To decline:** 1. Choose **Decline**. 2. Optionally type a reason. The requester sees it. 3. Choose **Confirm decline**. The requester gets an in-app notification that the request was declined, with your reason if you gave one. The reason isn't stored anywhere else. If two admins answer the same request at once, only the first answer counts. The other admin sees "That request has already been dealt with." ## Adding someone directly You don't have to wait for a request. Adding a person to the space from the **Spaces** tab also answers their pending request, and they get the same "You now have access" notification. See [Grant people and teams a role](/spaces/permissions#grant-people-and-teams-a-role). ## When requests are dropped A pending request disappears without a decision when it can no longer be answered: - the space stops being closed, because it was made open or private; - the space is deleted; - the requester leaves or is removed from the organisation; - the requester is added to the space directly (they're notified, as above). ## Limits Requesting access is rate-limited to six requests an hour per person, to stop admins being flooded with notifications. --- # Space activity The activity feed shows what has happened to the pages in a space: who created, edited, shared, deleted, restored or permanently deleted them, newest first. ## Open the feed - Choose **Activity** in the sidebar to see the full feed for the space you're in. - A space's **All Pages** home shows the latest entries under **Recent activity**. Choose **View all** to open the full feed. Anyone who can open the space can see its feed. Entries are grouped by day, and each one reads as a sentence, such as "Sam edited Deploy checklist". ![A space's activity feed, grouped under Today, Yesterday and earlier dates](/images/docs/screens/space-activity-light.webp) ## What's recorded | Entry | When it appears | | --- | --- | | **created** | Someone creates a page in the space. | | **edited** | Someone edits a page. | | **shared** | Someone shares a page with a person by email. The entry shows their access (**Read-only** or **Editor**). People who can manage the page's sharing also see who it was shared with. Everyone else sees "someone". | | **moved to the recycle bin** | Someone deletes a page. | | **restored** | Someone restores a page from the recycle bin. | | **permanently deleted** | Someone deletes a page forever, or empties the recycle bin. | Edits are grouped so that the feed stays readable: you see at most one "edited" entry per person per page every 15 minutes, however often the page autosaves. ## What you won't see The feed only shows entries for pages **you can open**. Two people looking at the same space can see different feeds: - Entries about a private page appear only for its author and the people it's shared with. - If a page is made private, or you otherwise lose access to it, its earlier entries disappear from your feed too. - Once a page is permanently deleted, its earlier entries are hidden for everyone. Only the "permanently deleted" entry remains, and it isn't shown at all if the page was private. ## How long entries are kept Entries are kept for 90 days, then removed automatically. For the organisation-wide record of security events, such as role changes and sharing, see the [Audit log](/organisation/audit-log). --- # Recycle bin Deleted pages go to a recycle bin, where they can be restored for 30 days before they're permanently removed. There's your own recycle bin, and an organisation-wide one for organisation admins. ## Deleting a page Choosing **Delete** on a page moves it to the recycle bin. It's not gone yet: it disappears from the space, search and public links, and waits in the bin. Only the page's author and space admins (including organisation admins) can delete a page. In an open space, members who aren't the author or an admin have no **Delete** option. See [Roles and permissions](/spaces/permissions). ## Your recycle bin Choose **Recycle bin** in the sidebar to open your own bin. It lists: - every page **you** deleted, in any space of the organisation, including My space; - every page deleted by anyone in the spaces **you administer**. It never shows pages from another member's My space. ![Your recycle bin, with the Space filter and Restore and Delete forever on each page](/images/docs/screens/recycle-bin-light.webp) Each row shows when the page was deleted and by whom. Use the **Space** filter at the top to narrow the list to one space. ## The organisation recycle bin Organisation admins also have **Recycle Bin** in the admin section of the organisation and space switcher. It lists every deletion across the organisation's shared spaces, with a **Space** filter. Members' My space deletions aren't listed: those belong to their owners alone. ![The organisation Recycle Bin in the hub, with Empty Bin and a Space filter in the column header](/images/docs/screens/org-recycle-bin-light.webp) ### Empty the bin 1. Optionally filter to one space. 2. Choose **Empty Bin**, or **Empty** followed by the space's name when you've filtered. 3. Confirm with **Empty Bin**. Every listed page is permanently deleted, except other members' private pages. Those are skipped, because only the person who deleted a private page can remove it. A message then tells you how many pages were deleted and how many were skipped. ## Restore a page Choose **Restore** next to the page. It goes back to its space, and to its group if the group still exists. ## Delete a page forever Choose **Delete forever** next to the page and confirm with **Delete forever**. This can't be undone. ## Who can restore and delete forever | Person | Can restore or delete forever | | --- | --- | | The person who deleted the page | Yes, if they're still its author or an editor of the space | | A space admin, including organisation admins | Any deleted page in that space | | Anyone else | No | A **private** page can only be restored or deleted forever by the person who deleted it. Rows you can't act on show a note explaining who can. ## Retention Deleted pages are kept for **30 days**. After that a nightly clean-up deletes them permanently. The bins remind you how long pages are kept. > [!WARNING] > Permanently deleted pages, whether removed by hand or after 30 days, can't be recovered. That includes their version history. ## Deleted spaces Deleting a space doesn't empty its recycle bin. Deleted pages move to the default space along with everything else, and can still be restored there. See [Delete a space](/spaces/creating-and-managing#delete-a-space). --- # Search Search finds pages by their title and content across every space you can open in the current organisation. It runs from the command palette, which you open with Ctrl K (⌘K on a Mac). ## Open search - Press **Ctrl K**, or **⌘K** on a Mac, from anywhere in the app. - Or choose **Search...** at the top of the sidebar. The command palette opens. Before you type, it lists your **Recent** pages. Start typing and it shows **Results** as you go. ![The command palette showing Results for a search, with the matching words highlighted in each extract](/images/docs/screens/search-light.webp) Use the arrow keys to move through the results and Enter to open one. Press Escape, or choose **Cancel**, to close the palette. Each result shows the page's title, its group and its space. When the match is in the page's content, a short extract appears with the matching words highlighted. ## What's searched Search covers pages in the organisation you're working in, in every space you can open. That includes My space and any page in the organisation that has been shared with you. It never shows a page you couldn't open, and pages in the recycle bin aren't included. Inside a page, search looks at: - the title; - paragraphs, headings, lists, quotes, tables and alert boxes; - code blocks and inline code; - the labels in diagrams; - image descriptions (alt text). It doesn't search the names in @mentions or the titles shown by links to other pages. Pages with a matching title are listed first, then pages that match in their content, best match first. ## How long new content takes to appear - **Titles** are searchable straight away. - **Content** becomes searchable about **30 to 40 minutes** after the page was last edited. Folioso waits until a page has been left alone for a while before updating the search index, so a page you're actively writing isn't re-indexed on every keystroke. Until then, search still finds the page by its title, and it may still find words you've just removed. Extracts are only shown for pages that haven't been edited since they were indexed, so text you've removed is never shown in a result. ## Tips for better results - **Every word must match.** Searching `deploy rollback` finds pages containing both words, in any order. - **The last word can be a prefix.** Typing `migr` finds "migration" and "migrate", so you can search mid-word. A single final letter is matched as a whole word. - **Prose is matched loosely.** Ordinary words match their other forms, so `deploying` also finds "deployed" and "deploys". - **Code is matched exactly.** Identifiers in code blocks and inline code, such as `useState` or `getUserById`, match as written, ignoring capitals. - **Keep it short.** Only the first eight words of a search are used. ## Known gaps A few kinds of search don't find what you'd expect: - **Dotted names and file paths.** A search for `config.php` or `app/Models/Page.php` usually misses, because search splits it into separate words while the page stores it as one. Search for other words on the page, or its title, instead. - **Mixing prose and code in one search.** A search that combines an ordinary word with an exact code identifier, such as `deploying getUserById`, can miss a page that contains both. Search for each one separately. - **Very recent edits.** Content edited in the last 30 to 40 minutes may not be found yet. See [How long new content takes to appear](/sharing/search#how-long-new-content-takes-to-appear). ## The command palette and the /search address There's no separate search results page. If you open `/search?q=your+words` in your browser's address bar, for example from a bookmark or a browser search shortcut, Folioso takes you to your current space and opens the command palette with your search filled in. Without `q`, it just takes you to your current space. To search from a script or another tool, use the REST API. See [Pages](/api/pages). --- # Sharing with people Share a single page with someone by email, as read-only or as an editor. They don't need to be in your organisation, and if they don't have a Folioso account yet, they're invited to create one. ## Who can share You can share a page if you're its author or an editor or admin of its space. On a [private page](/spaces/permissions#private-pages), only the author can share it. People a page has been shared with can't share it further. > [!IMPORTANT] > You need a verified email address to invite people. Until you've verified yours, the dialog shows "Verify your email address to invite people." with a **Verify now** link. ## Share a page 1. Open the page and choose **Share**. 2. Under **People with access**, type the person's email address. 3. Choose **Read-only** or **Editor**. 4. Choose **Invite**. ![The Share page dialog, with the email field and Invite above two people who have access](/images/docs/screens/share-dialog-light.webp) | Access | What the person can do | | --- | --- | | **Read-only** | Open and read the page. | | **Editor** | Open, read and edit the page's content, including live editing with others. | Shared access is to that one page only. It never gives a role in the space, so the person can't rename, move, tag, share or delete the page, and can't see other pages in the space. ## What the person receives **If they already have a Folioso account**, they get an in-app notification and an email saying you shared the page with them. The page appears in their **Shared with Me** list. See [Shared with you](/sharing/shared-pages). **If they don't have an account**, they get an invitation email. They appear in the dialog with a **Pending** badge. When they create a Folioso account with that same email address, the page is waiting in their **Shared with Me** list. If new sign-ups to Folioso are paused, a page invitation on its own doesn't let someone create an account. They'd need an invitation to join an organisation as well. > [!NOTE] > Sharing a page doesn't make someone a member of your organisation. To bring someone into the organisation, invite them from [Members](/organisation/members) instead. ### Resend an invitation For a pending person, choose the **Resend invitation** button (the paper plane) next to their address. The invitation is sent again in the name of the person who shared the page. ## Change or remove someone's access In the share dialog, use the select next to the person: - **Read-only** or **Editor** changes their access. It takes effect straight away, even if they have the page open. - **No Access** removes them. They lose access immediately. You can also revoke shares from **Shared by me**. See [Shared with you](/sharing/shared-pages). ## Shared page limits Your plan limits how many pages you can have shared at once. The limit counts **pages**, not people: sharing a page you've already shared with another person doesn't use another slot. Public links count too. Some plans have no limit. The count is per person, and the limit comes from the plan of the organisation you're working in. **Shared by me** shows how many you're using. To free a slot, revoke a share or turn off a public link. See the [pricing page](https://folioso.dev/pricing) for each plan's limit. ## Who can see the page in its space The **Private** / **Space** / **Public link** switch at the top of the share dialog decides who in the space can see the page, separately from the people you invite: - **Private**: "Only you and the people you invite." - **Space**: visible to everyone with a role in the space. - **Public link**: anyone with the link. See [Public links](/sharing/public-links). Only the author or a space admin can switch between **Private** and **Space**. See [Private pages](/spaces/permissions#private-pages). ## If a shared page is deleted When a page goes to the recycle bin, the people it's shared with can no longer open it. Restoring the page restores their access. Permanently deleting it removes the shares for good. --- # Public links A public link lets anyone with the URL read a page, without an account. You can make the link view-only or let signed-in people edit, protect it with a password, and turn it off at any time. ## Who can create a public link You can turn on a public link if you're the page's author or an editor or admin of its space. On a [private page](/spaces/permissions#private-pages), only the author can. > [!IMPORTANT] > You need a verified email address to publish a public link. Until then, the **Public link** option is disabled and the dialog offers **Verify now**. Turning a link off never needs verification. Each page has at most one public link. It counts towards your plan's [shared page limit](/sharing/sharing-with-people#shared-page-limits). ## Turn on a public link 1. Open the page and choose **Share**. 2. In the switch at the top of the dialog, choose **Public link**. 3. Choose **Copy** to copy the URL. ![The Share page dialog, with the Private, Space and Public link switch at the top](/images/docs/screens/share-dialog-light.webp) Turning on the link doesn't change who in the space can see the page. The dialog reminds you: "Also visible to everyone in…" for a space page, or "Not visible to your space — only people you invite." for a private one. The **Link preview** section shows the card that chat apps and social sites display when someone pastes the link. ## View or edit Use **Anyone with the link can** to choose: | Setting | Signed-out visitors | Signed-in visitors | | --- | --- | --- | | **View** | Read the page | Open the page in Folioso, read-only | | **Edit (sign-in required)** | Read the page, with a **Sign in to edit** prompt | Open the page in Folioso and edit it | Signed-out visitors always get a read-only copy of the page. When a signed-in person opens the link, Folioso takes them to the page in the app with the access the link gives, and lists it under **Opened from links** so they can find it again. See [Shared with you](/sharing/shared-pages). ![A page opened from a public link while signed out, read-only, with the organisation's name and Sign in in the header](/images/docs/screens/public-share-light.webp) ### Access belongs to whoever opened the link A public link only grants access to someone who has opened it. Being signed in to Folioso isn't enough: a person reaches the page in the app only after opening the link themselves, in that browser, and entering its password if it has one. They keep that access only while the link stays on with the same password. Turning the link off, switching it to **View** or changing the password takes effect for them straight away. ## Protect a link with a password 1. With the public link on, choose **Add a password** in the password box under the link. 2. Enter a **Link password** of 6 to 72 characters. 3. Choose **Save password**. The link now shows **Password protected**. Visitors see a "Password required" screen and must enter the **Link password** and choose **Unlock** before they see anything. The screen shows nothing about the page: no title, preview or branding. ![The Password required screen of a protected public link, with Link password and Unlock](/images/docs/screens/public-password-light.webp) - **Change password** sets a new one. Everyone who opened the link before has to enter the new password. - **Remove password** makes the link open to anyone who has it again. - Repeated wrong passwords are rate-limited. Correct ones never are. Someone who can already open the page another way, for example as a member of its space, goes straight through. If the link allows editing and they can only read the page, the prompt explains that the password unlocks edit access. ## Turn off a public link 1. Open the page and choose **Share**. 2. In the switch at the top of the dialog, choose **Private** or **Space**. 3. Confirm with **Disable link**. The URL stops working immediately for everyone who has it. Turning the link back on later creates a **new** URL, so the old one never works again. You can also turn off a link from **Shared by me** with **Revoke**. See [Shared with you](/sharing/shared-pages). ## When a link stops working Besides being turned off, a public link stops working while: - the page is in the recycle bin; - the organisation is scheduled for deletion. Its links work again if the organisation is reactivated. ## Search engines and link previews Pages opened from public links are marked so that search engines don't index them. A public link is for the people you send it to, not for discovery. Link previews still work: chat apps and social sites can show the page's title, a short extract and a preview card when someone pastes the link. Password-protected links show no preview of the page. If your organisation's plan includes custom branding and an admin has turned it on, public pages carry your organisation's branding. See [Organisation settings](/organisation/settings). --- # Shared with you Three lists in the sidebar keep track of sharing: pages shared with you, pages you opened from public links, and pages you've shared yourself. Find them under **Shared** in the sidebar. ## Shared with Me **Shared with Me** lists every page someone has shared with you by email, from any organisation. ![The Shared with Me list, with each page's title, who shared it, your access and the date](/images/docs/screens/shared-with-me-light.webp) Each row shows the **Title**, who it was **Shared By**, your **Access** (**Read-only** or **Editor**) and when it was **Shared**. Choose **Open** to go to the page. A page drops off the list when the person who shared it removes your access, or when its organisation is deleted. You don't need to be a member of the page's organisation to open a page shared with you. You get access to that page only. See [Sharing with people](/sharing/sharing-with-people). ## Opened from links **Opened from links** lists the pages you've opened from someone else's public link while signed in, so you can find them again. Each row shows the **Title**, who it was **Shared By**, the link's **Access** and when you **Opened** it. - Choose **Open** to go back to the page. - Choose **Remove** to take it off your list. This only tidies your list. It doesn't change the link. Opening a page from the list works only while the link is still on. See [Public links](/sharing/public-links). ## Shared by me **Shared by me** lists the shares you've created: public links, people you've shared with, and pending invitations. ![Shared by me, with the Shared pages meter above shares to people and public links, each with Revoke](/images/docs/screens/shared-by-me-light.webp) At the top, the **Shared pages** meter shows how many pages you have shared against your plan's limit. See [Shared page limits](/sharing/sharing-with-people#shared-page-limits). Each row shows the **Title**, who it's **Shared with** (a person, or the public link), the **Access**, the **Protection** (whether a public link has a password) and when it was **Shared**. ### Revoke a share 1. Choose **Revoke** on the row. 2. Confirm with **Revoke**. Revoking a person's share removes their access straight away. Revoking a public link turns it off: the URL stops working permanently, and turning the link on again creates a new one. **Revoke** appears only for pages in the organisation you're working in. For a page in another organisation, choose **Open** and change the share from the page's share dialog. ### Pages counted but not listed If you've lost access to a page you shared, for example because it was moved to a space you're not in, the share still counts towards your limit but isn't listed. The page tells you how many shares are counted but not listed. --- # Notifications Folioso tells you when something needs your attention: a page shared with you, a mention, or a request to join a space you look after. Some notifications appear in the app, some arrive by email, and a few do both. ## The notification bell The bell in the header at the top of each page shows a badge with your unread count. New in-app notifications appear as they happen, without reloading the page. - Select the bell to see your latest notifications. Opening it marks them all as read. - Select a notification to go to the page or screen it's about. - Choose **Dismiss** next to a notification to remove it from the bell, or **Clear all** to remove them all. - Choose **View all** to open the **Notifications** page. ## The Notifications page The **Notifications** page lists your full history, newest first, including notifications you've dismissed from the bell (shown faded). Unread ones are highlighted. ![The Notifications page, with an access request, a page shared with you and a mention](/images/docs/screens/notifications-light.webp) Notifications are kept for 90 days, then removed automatically, whether or not you've read them. ## In-app notifications | You're notified when | Who receives it | Also emailed? | | --- | --- | --- | | Someone shares a page with you | The person it's shared with | Yes | | Someone mentions you on a page with **@** | The person mentioned | No | | A member asks to join a closed space | Every admin of the space, including organisation admins | Yes | | Your request to join a space is approved, or you're added directly while a request is pending | The requester | No | | Your request to join a space is declined | The requester, with the admin's reason if they gave one | No | You get one mention notification per page, however many times you're mentioned on it. Mentioning yourself normally doesn't notify you. See [Links and mentions](/editor/links-and-mentions). ## Email-only messages Some messages come by email only, because they're about your account or need you to act outside the app: - invitations to join an organisation, and invitations to a page for someone without an account; - email verification and password reset links; - a welcome email when you create an account; - a link to your data export when it's ready (see [Exporting your data](/account/exporting-your-data)); - a link to a PDF export, if you closed the export dialog before downloading it (see [Exporting](/editor/exporting)); - notices about your account or organisation being scheduled for deletion; - notices when ownership of an organisation is transferred; - notices when your membership is suspended, and, for admins, when members are suspended because the organisation is over its seat limit; - billing emails such as overdue invoice reminders. ## Email settings There are no per-notification email settings. Emails go to your account's email address. To change it, see [Profile](/account/profile). > [!NOTE] > If you're [suspended](/organisation/suspending-members) in an organisation, you don't receive that organisation's emails, apart from the notice telling you you've been suspended. In-app notifications are still kept. --- # Organisation overview The organisation hub is where organisation admins run the organisation: its name and logo, members, teams, spaces, billing, uploads, the recycle bin, the audit log and settings. ## What an organisation is Your organisation is your team's own Folioso workspace. It has its own web address, and it holds your spaces, teams, members and subscription. Every account gets an organisation when it registers, and you can belong to several. See [Working in several organisations](/organisation/multiple-organisations). Everyone in an organisation is either an **admin** or a **member**. Admins manage the organisation; members work in its spaces. See [Admins and ownership](/organisation/roles-and-ownership). ## Who can open the hub | Hub tab | Who can open it | | --- | --- | | Overview, Members, Teams, Billing, Uploads, Recycle Bin, Settings | Organisation admins | | Spaces | Organisation admins, and members who are an admin of at least one space | | Audit log | Organisation admins, if your plan includes the audit log | A member who opens a hub page they aren't allowed to see gets an error page. ## Opening the hub 1. Click the organisation card at the top of the sidebar. It shows your organisation's name and the space you're in. 2. Under **Admin**, choose **Overview**, **Members**, **Teams**, **Spaces**, **Billing**, **Recycle Bin**, **Audit log** or **Settings**. Inside the hub, the left-hand rail (a scrolling strip on a phone) switches between tabs. It also has an **Uploads** tab. Close the hub with the close button in the header to go back to your spaces. ![The organisation overview with its name, logo, address and administrators](/images/docs/screens/org-overview-light.webp) ## The Overview tab The Overview tab shows: - **Name**: click **Rename** to change it, then **Save**. Names can be up to 100 characters. - **Logo**: upload a square logo, or remove it. The logo shows in the sidebar, the organisation switcher and the hub header. It only appears on shared pages if you turn on [branding](/organisation/settings#branding). - **Address**: your organisation's web address. **Change** takes you to [Settings](/organisation/settings#web-address). - **Created**: the date the organisation was created. - **At a glance**: counts of members, teams, spaces and pages. Click a card to open that tab. - **Administrators**: everyone with admin access, and the date they became an admin. If you're the only admin of an organisation with other members, the Overview warns you. Promote another member so the organisation can still be managed if you leave. See [Admins and ownership](/organisation/roles-and-ownership#changing-a-role). ## The other tabs - [Members](/organisation/members): invite people, change roles, suspend or remove members. - [Teams](/organisation/teams): group members so you can grant a whole team access to a space. - [Spaces](/organisation/managing-spaces): set access for every space you manage. - [Billing](/billing/plans): your plan, seats, invoices and billing details. - [Uploads](/organisation/uploads): every image in your organisation's pages, and how much storage you've used. - [Recycle Bin](/spaces/recycle-bin): deleted pages from every shared space. - [Audit log](/organisation/audit-log): a record of security-relevant changes. - [Settings](/organisation/settings): name, web address, branding, single sign-on, email domains, automatic suspension, ownership and deleting the organisation. > [!NOTE] > Admins and owners of an organisation with more than one member must use two-factor authentication. See [Two-factor authentication](/account/two-factor-authentication). --- # Organisation settings The Settings tab is where organisation admins change the organisation's name, web address and branding, and reach single sign-on, email domains, automatic suspension, ownership and deletion. ## Opening Settings Only organisation admins can open Settings. Click the organisation card at the top of the sidebar, then choose **Settings** under **Admin**. ![Organisation settings with the name, web address and branding sections](/images/docs/screens/org-settings-light.webp) ## Name The name is shown in the sidebar, the organisation switcher and invitation emails. 1. Under **Name**, type the new **Organisation name** (up to 100 characters). 2. Click **Save name**. You can also rename the organisation from the [Overview tab](/organisation/overview#the-overview-tab). ## Web address Every organisation has its own web address, made of a part you can choose followed by Folioso's domain. A new organisation gets an address like `org123456`. Choosing your own address is available if your plan includes a custom organisation address. Otherwise the field is read-only, marked **Pro**, and **Upgrade to choose** takes you to Billing. To change it: 1. Under **Web address**, type **Your part of the address**: 3 to 32 lowercase letters, digits and hyphens. It can't start or end with a hyphen. 2. Click **Change address**. Folioso takes you straight to Settings at the new address. Everyone visiting your organisation sees the new address from now on. What to expect: - **The old address keeps working for 30 days.** Bookmarks and links in old emails redirect to the new address. Nobody else can claim the old address during that time. - **You can go back.** Changing back to an address you used in the last 30 days is allowed. - **You can change it up to three times a day.** After that, wait until tomorrow. - Some words are reserved, and you can't take an address another organisation uses or still redirects from. - If your plan later stops including this feature, your chosen address stays. You just can't change it again until you upgrade. > [!WARNING] > After 30 days the old address stops redirecting. Update any links you've put in other tools before then. ## Branding Branding puts your organisation's logo, name and accent colour on the pages you share publicly. It's available if your plan includes custom branding. On other plans the section is read-only, and shared pages carry Folioso's own branding. 1. Upload your logo on the [Overview tab](/organisation/overview#the-overview-tab) if you haven't already. 2. Under **Branding**, tick **Show our logo and name on shared pages**. It's off by default. 3. Optionally, set an **Accent colour**: pick one or type a hex value such as `#E0A33E`. Leave it empty to use Folioso's amber. 4. Click **Save branding**. The **Shared page header** preview shows what a guest sees. Folioso picks black or white text on your colour so it stays readable. Where branding appears: - the header of every page opened from one of your [public links](/sharing/public-links); - the preview card shown when someone pastes one of your public links into a chat or social site. The password prompt of a protected link never shows branding. If your plan stops including branding, shared pages go back to Folioso's branding straight away. Your settings are kept for when you upgrade again. > [!NOTE] > Uploading a logo doesn't publish it. Your logo only appears on shared pages once you tick **Show our logo and name on shared pages**. ## Other settings on this tab - **Single sign-on**: sign in through your own identity provider. See [Single sign-on](/organisation/single-sign-on). - **Email domains**: let colleagues join with their company email address. See [Verified domains](/organisation/domains). - **Automatic suspension**: suspend members who stop visiting. See [Suspending members](/organisation/suspending-members#automatic-suspension). - **Ownership**: see who owns the organisation and transfer it. See [Admins and ownership](/organisation/roles-and-ownership#transferring-ownership). - **Danger zone**: delete the organisation. See [Deleting an organisation](/organisation/deleting-an-organisation). Plans are compared on the [pricing page](https://folioso.dev/pricing). --- # Members and invitations Organisation admins invite people by email, track pending invitations and manage everyone in the organisation from the Members tab. ## The Members tab Only organisation admins can open Members. Click the organisation card at the top of the sidebar, then choose **Members** under **Admin**. The header shows how many people are in the organisation, how many are suspended and, on a flat plan, how many seats are used. Each row of the table shows: | Column | What it shows | | --- | --- | | Member | The member's name, with **you** beside your own row and a **Suspended** badge for a suspended member, and their email address underneath. A long address is shortened to fit; hover over it to see it in full. | | Role | **Member** or **Admin**. | | Joined | When they joined, and "via" a domain if they joined through a [verified domain](/organisation/domains). | | Last visited | Their last visit to this organisation, on the web or through the API. | | Actions | The **…** menu at the end of each row: **Suspend**, **Unsuspend** and **Remove**. Your own row has **Leave** instead. | ![The Members tab, with the invite form, a pending invitation and the members table](/images/docs/screens/org-members-light.webp) ## Inviting someone 1. Under **Invite by email**, type their address. 2. Choose a **Role**: **Member** or **Admin**. 3. Click **Send invitation**. Folioso emails them a link to join. The invitation appears under **Pending invitations** until they accept it. Before you can invite: - **Your own email address must be verified.** - **Two-factor authentication:** admins of an organisation with other members must use two-factor authentication. If you're the only member, set it up before you invite your first person. See [Two-factor authentication](/account/two-factor-authentication). You can't invite someone who is already a member, or an address that already has an invitation waiting. ### Seats and invitations How invitations count against seats depends on your plan: - **Flat plans** have a fixed number of seats. Every member and every pending invitation that hasn't expired takes one. Suspended members don't. When every seat is taken, the button changes to **Upgrade plan** and takes you to Billing. - **Per-seat plans** are billed per seat, so you can always invite. When someone accepts, your seat count goes up. See [Seats](/billing/seats). ## What the invited person does They open the link in the email. - **Not signed in:** they're asked to sign in, with their address filled in. Someone new to Folioso can create an account instead. - **Signed in with the invited address:** they join straight away and land in your organisation. - **Signed in with a different address:** they see a page explaining the mismatch and can sign out and try again with the right account. If the organisation has no free seat when they accept, Folioso can't add them. They're asked to get an admin to free a seat and then open the link again. ## Managing pending invitations Each pending invitation shows the address, role, who sent it and when it expires. - **Invitations expire after 7 days.** An expired invitation stays in the list, marked expired. - **Resend** emails a fresh link and resets the expiry to 7 days from now. The previous link stops working. You can resend an expired invitation. - **Revoke** cancels the invitation. The link stops working, and you can invite that address again. ## Removing a member 1. Find the member, open the **…** menu at the end of their row and choose **Remove**. 2. Confirm with **Remove**. ![The actions menu open on a member's row, with Suspend and Remove](/images/docs/screens/org-member-actions-light.webp) The member loses access to the organisation straight away. What happens next: - Their pages stay where they are. Private pages and pages in their personal space appear under [Former members' content](/organisation/former-members-content). - Their personal space is deleted if it was empty. - They're taken off every team and every space grant in this organisation, their pending space access requests are dropped, and their API tokens for this organisation are deleted. - On a per-seat plan, the freed seat comes off your bill at the next billing period. See [Seats](/billing/seats). - **They can't rejoin through a verified domain.** Only a new invitation lets them back in. See [Verified domains](/organisation/domains#people-you-removed). You can't remove the owner, or the last admin of an organisation that has other members. See [Admins and ownership](/organisation/roles-and-ownership). > [!TIP] > To take someone's access away without losing their setup, [suspend them](/organisation/suspending-members) instead. Suspension keeps their teams and space grants for when you unsuspend them. ## Changing a member's role Use the **Role** dropdown on their row. See [Admins and ownership](/organisation/roles-and-ownership#changing-a-role) for the rules. ## Leaving To leave an organisation yourself, click **Leave** on your own row, or **Leave organisation** in the sidebar's organisation card. See [Admins and ownership](/organisation/roles-and-ownership#leaving-an-organisation). --- # Admins and ownership Every member of an organisation is an admin or a member, and one admin is the owner: the billing contact who can't be removed until they hand the organisation on. ## Admins and members | | Member | Admin | | --- | --- | --- | | Work in open spaces, and in closed or private spaces they've been granted | ✓ | ✓ | | Admin role on every shared space | — | ✓ | | Open the organisation hub | Spaces tab only, if they're a space admin | ✓ | | Invite, suspend and remove members, change roles | — | ✓ | | Create spaces and teams | — | ✓ | | Manage billing | — | ✓ | | Change organisation settings, or delete the organisation | — | ✓ | An organisation admin is an admin of every shared space, but not of other people's personal spaces. Nobody but its owner can open a personal space ("My space"). Private pages belong to their author and the people they're shared with. Admins only reach a former member's private pages through [Former members' content](/organisation/former-members-content). For space roles, see [Space permissions](/spaces/permissions). > [!IMPORTANT] > Admins and owners of an organisation with more than one member must use two-factor authentication. See [Two-factor authentication](/account/two-factor-authentication). ## Changing a role Only organisation admins can change roles. 1. Open the **Members** tab. 2. In the member's **Role** dropdown, choose **Admin** or **Member**. The change applies straight away. When an admin becomes a member, they lose admin access to every space at once and keep only the access they hold through their own space grants and teams. The rules: - **You can't change your own role.** Another admin has to do it. - **The last admin can't be demoted.** If other members remain, promote someone else first. The error reads "Promote another admin first." - **The owner can't be demoted.** Transfer ownership first. - **A suspended member can't be promoted.** Unsuspend them first. ## The owner The owner is the organisation's billing contact of record. Whoever registers the organisation is its first owner. The owner must be an admin. While they're the owner, they can't: - be demoted to member; - be removed from the organisation; - be suspended; - leave the organisation. Settings shows the current owner under **Ownership**. ## Transferring ownership Only the owner can transfer the organisation, and only to someone who is already an admin. 1. If the person isn't an admin yet, make them one on the **Members** tab. 2. Open **Settings** and go to **Ownership**. 3. Under **Transfer to**, choose the admin. 4. Click **Transfer ownership**. The transfer is immediate. The new owner doesn't need to accept it. Both of you are notified. You stay an admin, so you can now be demoted or leave like anyone else. If there's nobody to choose, Settings says "There's no one to transfer to yet — promote another member to admin first." ### Claiming an ownerless organisation If the owner's account is deleted, the organisation is left with no owner, and Settings shows "No owner — any admin can claim this organisation." Any admin can then choose themselves (or another admin) under **Transfer to** and click **Transfer ownership**. ## Leaving an organisation To leave: 1. Click the organisation card at the top of the sidebar and choose **Leave organisation**, or click **Leave** on your own row of the **Members** tab. 2. Confirm with **Leave**. You lose access straight away. Your pages stay where they are for the remaining members, and Folioso takes you to another of your organisations. You can't leave: - **your only organisation.** To leave Folioso altogether, [delete your account](/account/deleting-your-account); - **an organisation where you're the only member.** Invite someone first, or delete your account; - **as the last admin** while other members remain. Promote someone else first; - **as the owner.** Transfer ownership first. Leaving doesn't stop you rejoining later through a [verified domain](/organisation/domains). --- # Suspending members Suspending a member takes away their access to the organisation without removing them, and frees their seat; you can suspend people by hand or automatically after a period without a visit. ## Suspension or removal? | | Suspend | Remove | | --- | --- | --- | | Access to the organisation | Blocked until unsuspended | Gone | | Takes a seat | No | No | | Teams and space grants | Kept, and restored on unsuspension | Removed | | API tokens for this organisation | Kept, but refused while suspended | Deleted | | Their pages | Stay where they are | Stay where they are | | Undo | **Unsuspend** | Invite them again | Suspending members, by hand or automatically, is available if your plan includes member suspension. See the [pricing page](https://folioso.dev/pricing). Unsuspending is always possible, whatever your plan. ## Suspending a member by hand Only organisation admins can suspend. 1. Open the **Members** tab. 2. Open the **…** menu at the end of the member's row and choose **Suspend**. 3. Confirm with **Suspend**. Their row shows a **Suspended** badge. A member suspended by hand isn't emailed. You can only suspend people with the **Member** role. To suspend an admin, change their role to member first. The owner can never be suspended. ## What a suspended member sees When a suspended member opens the organisation, they see "Your account in [organisation] is suspended" and are asked to contact an administrator. The page links to their other organisations and has a **Sign out** button. In the organisation switcher, the organisation is marked **suspended**. While suspended, they: - can't open any space or page in the organisation, including their own personal space and pages shared with them; - can't use the organisation through the API; - get no organisation emails, apart from the notice that they've been suspended automatically. Their pages stay where they are, and colleagues can still mention them. ![The page a suspended member sees, with Your other organisations and Sign out](/images/docs/screens/suspended-light.webp) ## Unsuspending a member 1. Open the **…** menu at the end of the member's row and choose **Unsuspend**. 2. Confirm with **Unsuspend**. They get back exactly the access they had, including their teams and space grants, and they take a seat again: - **Per-seat plans:** your seat count goes up, which may raise your bill. See [Seats](/billing/seats). - **Flat plans:** you need a free seat. If every seat is taken, you're asked to upgrade. Unsuspending resets their last visit to today, so automatic suspension won't catch them again the same night. ## Automatic suspension Automatic suspension suspends members who haven't visited the organisation for a number of days you choose, so they stop taking a seat. A visit counts on the web or through the API. To turn it on: 1. Open **Settings** and go to **Automatic suspension**. 2. Tick **Suspend members who haven't visited for a while**. 3. Set **Days without a visit**: between 31 and 3,650 days. The suggested value is 90. 4. Click **Save**. Folioso checks every night. Each member caught by it gets one email saying they've been suspended. Admins and the owner are never suspended automatically. To turn it off, untick the box and click **Save**. ![The Automatic suspension section of organisation Settings, turned on with Days without a visit filled in](/images/docs/screens/org-auto-suspend-light.webp) > [!NOTE] > If your plan stops including member suspension, automatic suspension is paused, not deleted. Settings shows the saved value, and **Turn off automatic suspension** lets you switch it off. Members who were already suspended stay suspended until you unsuspend them. ## Suspensions when you move to a smaller plan Moving to a flat plan, or returning to Free, while you have more members than it allows suspends the excess members when the change takes effect. The owner is always kept. Each suspended member is emailed, and the remaining admins are told who was suspended. See [Downgrading](/billing/downgrading). ## In the audit log Suspensions and unsuspensions are recorded in the [audit log](/organisation/audit-log), with automatic suspensions marked as such. --- # Teams A team is a named group of members that you can grant a role on a space in one step, instead of adding each person separately. ## How teams work A team only groups people. On its own it gives no access. Its power comes from space grants: give a team the Viewer, Editor or Admin role on a closed or private space, and everyone in the team gets that role there. When someone joins or leaves the team, their access follows. If a person has a role through more than one route, such as their own grant and one or more teams, they get the highest of those roles. Open spaces make every member an editor anyway, so team grants matter on closed and private spaces. See [Space permissions](/spaces/permissions). Only organisation admins can create and manage teams. ## How many teams you can have Your plan sets how many teams the organisation can have, and some plans have no limit. When you reach your plan's limit, creating another team asks you to upgrade. See the [pricing page](https://folioso.dev/pricing) for each plan's limit. ## Creating a team 1. Click the organisation card at the top of the sidebar and choose **Teams** under **Admin**. 2. Under **New team**, type a name (up to 120 characters). 3. Click **Create team**. ![The Teams tab, with the New team form above team cards listing each team's members and granted spaces](/images/docs/screens/org-teams-light.webp) ## Adding and removing team members Each team card has a **Members** list. - **To add someone**, choose them in **Add a member…** and click **Add**. - **To take someone off**, click the remove button beside their name. ## Giving a team access to a space You grant a team a role from the space, not from the team: 1. Open the [Spaces tab](/organisation/managing-spaces) and find the space. 2. Under **Teams**, choose the team in **Add a team…**, pick a role and click **Add**. You can also do this from the space's own settings. Each team card lists its **Granted spaces** and the role it holds on each. ## Renaming, logos and deleting - **Rename:** click the pencil button on the team card, type the new name and click **Save**. - **Logo:** upload a square logo on the team card, or remove it. - **Delete:** click the bin button on the team card and confirm with **Delete**. Members lose the access they held through the team. Their own memberships and direct space grants stay. When someone leaves or is removed from the organisation, they're taken off all its teams. A [suspended](/organisation/suspending-members) member stays on their teams, but has no access while suspended. --- # Managing spaces The hub's Spaces tab lists every space you manage, so you can set each one's access type, answer access requests and grant people and teams roles in one place. ## Who can open the Spaces tab - **Organisation admins** see every shared space in the organisation. - **Members who are an admin of one or more spaces** see just those spaces. For them, Spaces is the only hub tab they can open. Personal spaces ("My space") are never listed. To open the tab, click the organisation card at the top of the sidebar and choose **Spaces** under **Admin**. ![The Spaces tab with access settings, people and teams for each space](/images/docs/screens/org-spaces-light.webp) ## What each space shows Each space has a card with: - its name and logo, a **Default** badge on the default space, and its access type; - how many people have access; - **Open**, which takes you to the space; - **Access**, **Requests**, **People** and **Teams** sections, described below. To change a space's logo, upload one on its card or remove it. ## Setting the access type Under **Access**, choose: | Access type | Who gets in | | --- | --- | | **Open** | Every member of the organisation can read and edit pages here. | | **Closed** | Listed in the space directory, but only people and teams you grant access to can get in. | | **Private** | Invisible to everyone except the people and teams you grant access to. | Private spaces are available if your plan includes them. The default space is always open, so its access type can't be changed. See [Spaces overview](/spaces/overview). ## Answering access requests When someone asks to join a closed space, their request appears under **Requests**. - **To approve**, choose the role to give them and click **Approve**. - **To decline**, click **Decline**, optionally type a reason (the requester sees it) and click **Confirm decline**. See [Access requests](/spaces/access-requests). ## Granting people and teams Grants only matter on closed and private spaces. While a space is open, every member is an editor. **People:** 1. In **Add a person…**, choose a member. 2. Pick a role: **Viewer**, **Editor** or **Admin**. 3. Click **Add**. **Teams:** 1. In **Add a team…**, choose a team. 2. Pick a role. 3. Click **Add**. To change a role, use the dropdown beside the person or team. To take a grant away, click the remove button beside it. If there are no teams yet, create one on the [Teams tab](/organisation/teams). For what each role allows, see [Space permissions](/spaces/permissions). ## Creating a space Organisation admins can click **New space** at the top of the tab. When you've reached your plan's space limit, the button shows **Upgrade** and takes you to Billing. Your plan sets how many shared spaces you can have; see the [pricing page](https://folioso.dev/pricing). Personal spaces don't count towards the limit. For creating, renaming and deleting spaces, see [Creating and managing spaces](/spaces/creating-and-managing). --- # Verified domains Verify your company's email domain and anyone with a confirmed address on it can join your organisation as a member, without an invitation. ## Before you start - Email domains are available if your plan includes domain auto-join. See the [pricing page](https://folioso.dev/pricing). - Only organisation admins can add, change or remove domains. - Your own email address must be verified. - You need someone who can read a mailbox on the domain, such as `it@acme.com`, to confirm it. ## Adding and verifying a domain 1. Open **Settings** and go to **Email domains**. 2. Fill in: - **Domain**: for example `acme.com`. - **Verify through**: an email address on exactly that domain. An address on a subdomain, such as `it@eu.acme.com` for `acme.com`, isn't accepted. - **Member limit**: the most people who can join through this domain. It starts at 10. 3. Click **Add domain**. Folioso emails a confirmation link to that address. The domain shows "Waiting for verification" until someone opens the link and clicks **Confirm** followed by the domain name. The link works for 48 hours. Nothing changes unless they confirm. ![The Email domains section of organisation Settings, with the add form above a verified domain and its Allow joining switch and Member limit](/images/docs/screens/org-domains-light.webp) What you can't claim: - public email providers such as gmail.com; - a domain another organisation has already verified. ### Resending the confirmation If the email didn't arrive or the link expired, use **Send the link to** under the domain, change the address if you need to, and click **Resend**. Only the newest link works. ## How people join Once a domain is verified, joining through it is switched on. Anyone whose account has a **verified** email address on that domain is offered to join: - straight after they sign up or sign in, on a page headed "Your team is already here"; - in the organisation switcher in the sidebar, as **Join** followed by your organisation's name. They click **Join** and become a **Member**. They never join as an admin. Joining is always the person's choice; nobody is added automatically. On the Members tab, people who joined this way show "via" the domain under **Joined**. ## Member limits and seats Each domain shows how many people have joined through it out of its limit, for example "4 of 10 joined". The count is of current members who joined through the domain, so when one leaves, a place frees up. When the limit is reached, people can still see the offer, but joining is refused with an explanation. To change the limit, edit **Member limit** under the domain and click **Save**. Each person who joins takes a seat, exactly as if they'd accepted an invitation, so your seat count goes up. See [Seats](/billing/seats). ## Pausing or removing a domain - **To pause joining**, switch off **Allow joining** under the domain. Switch it on again to resume. - **To remove a domain**, click **Remove** and confirm with **Remove domain**. Nobody new can join through it. Members who already joined stay. Removing a verified domain frees it, so another organisation could verify it. ## People you removed When an admin removes someone from the organisation, that person can't rejoin through your domains. To let them back in, invite them. Accepting an invitation lifts the block. People who leave on their own aren't blocked. ## If your plan changes If your plan stops including email domains, nobody can join through them, and you can't add domains or change their settings. Your domains stay listed, and you can still remove them. --- # Single sign-on Single sign-on lets your members sign in to Folioso through your own OpenID Connect identity provider, such as Google Workspace, Microsoft Entra ID or Okta. ## Before you start - Single sign-on is available if your plan includes it. See the [pricing page](https://folioso.dev/pricing). - Only organisation admins can set it up. - Folioso supports OpenID Connect (OIDC). SAML isn't supported. - Your provider must say that each member's email address is verified. Folioso refuses a sign-in without that. ## How it works Single sign-on works alongside password sign-in and never replaces it. Members who prefer their password, or who sign in with GitHub, can carry on doing so. A misconfigured provider can't lock anybody out. Single sign-on only signs in people who are **already members** of your organisation. It doesn't create accounts. Invite people first, as usual. See [Members and invitations](/organisation/members). The first time a member signs in through your provider, Folioso links their provider identity to their Folioso account by email address. Both must be verified: - the provider must confirm the email address is verified; - the member's Folioso account must already have a confirmed email address. If it doesn't, they're asked to sign in with their password once to confirm it. After that first sign-in, the link stays in place even if their email address changes later. ## Setting it up 1. Open **Settings** and go to **Single sign-on**. 2. In your identity provider, create an OpenID Connect web application. Give it the **redirect URI** shown in Folioso under "Give your provider this redirect URI". Copy it exactly. 3. Back in Folioso, fill in: - **Issuer URL**: exactly as your provider publishes it, for example `https://accounts.google.com`. It must start with `https://`. Folioso reads the provider's discovery document from there. - **Client ID**: from your provider. - **Client secret**: from your provider. 4. Leave **Allow members to sign in with single sign-on** unticked for now, and click **Save single sign-on**. 5. When you're ready, tick **Allow members to sign in with single sign-on** and click **Save single sign-on** again. Folioso asks your provider for the `openid`, `email` and `profile` scopes. ![Single sign-on settings with the issuer, client ID and redirect URI](/images/docs/screens/org-sso-light.webp) After saving, the secret is stored and never shown again. The field reads "Stored — leave blank to keep it". Type a new secret only when you want to replace it. ## How members sign in Share the address shown under "Members sign in from" with your members. Opening it takes them straight to your provider. Or, on the Folioso sign-in page: 1. Click **Single sign-on**. 2. Type the **Workspace address**: the first part of your organisation's web address. 3. Click **Continue** and sign in at the provider. After signing in at the provider, they land in your organisation. Signing in through single sign-on doesn't ask for a Folioso two-factor code. Single sign-on checks membership every time. Someone you've removed from the organisation can't sign in through it, even if your provider still recognises them. ## When sign-in fails The sign-in page explains what went wrong. Common causes: - "No [organisation] account uses that address": the person isn't a Folioso user with that email address. Invite them first. - "That address has not been confirmed on this account yet": they need to sign in with their password once to confirm their email address. - "That account is no longer a member of [organisation]": they have a Folioso account but aren't a member of your organisation, for example because they've been removed. - "Single sign-on is not available for that workspace": it's switched off, not set up, or your plan doesn't include it. ## Turning it off Untick **Allow members to sign in with single sign-on** and click **Save single sign-on**. Members sign in with their password or GitHub instead. If your plan stops including single sign-on, it stops working until you upgrade. Every change to the single sign-on settings is recorded in the [audit log](/organisation/audit-log). --- # Audit log The audit log records who changed access, membership, sharing and settings in your organisation, so admins can answer "who did this, and when?" ## Who can see it Reading and exporting the audit log is available to organisation admins if your plan includes it. On other plans, the **Audit log** tab is hidden. See the [pricing page](https://folioso.dev/pricing). Folioso records events on every plan. If you upgrade to a plan that includes it, the log already shows what happened before you upgraded, up to the retention period. If your plan changes and stops including it, nothing is lost: the log reappears when you upgrade again. ## Opening the audit log Click the organisation card at the top of the sidebar and choose **Audit log** under **Admin**. Entries are listed newest first. ![The audit log, newest first, with filters in the Event and Who column headers and Export CSV](/images/docs/screens/org-audit-light.webp) ## What's recorded | Area | Events | | --- | --- | | Invitations | Invitation sent, resent, revoked, accepted | | Members | Member removed, member left, role changed, ownership transferred, email address changed, password changed, joined via email domain, member suspended, member unsuspended | | Spaces | Space created, renamed, deleted, access type changed, access granted, access revoked, access request approved, access request declined | | Teams | Team created, renamed, deleted, team member added, team member removed | | Sharing | Page shared, share revoked, share permission changed, link password set, link password cleared, public link enabled, public link disabled | | Pages | Page visibility changed, page moved to another space, orphaned page taken over, orphaned page deleted, recycle bin emptied | | Organisation | Organisation renamed, web address changed, branding changed, single sign-on changed, deletion requested, organisation reactivated | | Email domains | Email domain added, verified, settings changed, removed | The log doesn't record everyday editing; for that, see a space's [activity feed](/spaces/activity). Each entry shows the event, who did it, the details and when. Automatic events, such as members suspended for inactivity or an email domain verified, read **Automatic** in **Who** and give the reason in their details. The person's name and email are saved with the entry, so a colleague who has since left is still named. ### What the log keeps private The log never names content an admin couldn't open: - Nothing is recorded for pages in someone's personal space. - For a private page, the entry shows that the page is private, but not its title. A reference number tells entries for different pages apart. - Single sign-on changes record whether the client secret was replaced, never the secret itself. ## Filtering Filter from the column headers: - **Event**: one type of event. - **Who**: one person, **Automatic**, or **Former member**. - **When**: a date range. **Clear filters** removes them all. ## Exporting Click **Export CSV** to download the log as a CSV file. The export includes the filters you've applied, so it contains what's on screen. ## How long entries are kept Entries are kept for 365 days, then deleted. The page shows the retention period beside the description. --- # Uploads and storage The Uploads tab shows every image in your organisation's pages and how much of your plan's image storage you've used. ## Opening Uploads Only organisation admins can open Uploads. Click the organisation card at the top of the sidebar and choose **Uploads** under **Admin**, or choose it in the hub's left-hand rail (see [Organisation overview](/organisation/overview#opening-the-hub)). ![The Uploads tab, with storage used above image tiles showing size, dimensions, page, uploader and date](/images/docs/screens/org-uploads-light.webp) ## The gallery Images are listed newest first. Each tile shows the image with its file size, dimensions, the page it's on, who uploaded it and when. A tile marked **In recycle bin** belongs to a page in the recycle bin. Click a tile to open the page, or the saved page version, that holds the image. Images on pages you can't open, such as someone else's private page, appear as **Private image** with their size only, marked "Not visible to you". Being an admin doesn't let you see inside other people's private pages. Organisation logos, team and space logos and profile pictures aren't listed and don't count towards storage. ## How storage is counted The figure at the top shows how much you've used against your allowance, for example "*used* of *allowance* used". Storage is pooled: everyone in the organisation shares one allowance, worked out per seat. Your plan sets the amount per seat; see the [pricing page](https://folioso.dev/pricing). - **Per-seat plans** multiply the per-seat amount by the number of seats you're billed for, so the allowance grows as members join. See [Seats](/billing/seats). - **Flat plans** count as one seat, whatever the number of members. - Images on pages in the recycle bin still count until the pages are permanently deleted. If your plan has no storage limit, the page says "No storage limit on this plan". ## When you run out When an upload would take the organisation over its allowance, the upload is refused and the editor explains why. Existing images are never removed. To make room, upgrade your plan, or delete pages you no longer need and remove them from the recycle bin for good. Each image also has a maximum size per upload, set by your plan. See [Images](/editor/images) and the [pricing page](https://folioso.dev/pricing). --- # Former members' content When someone leaves your organisation, their private pages and the pages in their personal space stay behind; organisation admins can take them over or delete them here. ## Why content is left behind Removing a member, or a member leaving, never deletes their pages. Pages they wrote in shared spaces stay where they are, and everyone with access to those spaces keeps using them. Two kinds of page, though, are invisible to everyone else: - **private pages** they wrote in shared spaces; - **pages in their personal space** ("My space"). Once the author is no longer a member, nobody could reach these pages. This view lets an organisation admin decide what happens to them. It's the only place where admins can see other people's private pages, and only once the author has gone. A personal space that held nothing is deleted when its owner leaves. One with pages or groups is kept until you deal with its contents. If a member deletes their Folioso account, their personal spaces are erased with it, so those pages never appear here. Their private pages in shared spaces do appear, with **Former member** as the author. ## Opening the view Only organisation admins can open it. 1. Open the **Members** tab of the organisation hub. 2. Click **Content left by former members**. ![Content left by former members, listing a private page in a shared space and a page in a personal space, each with an actions menu](/images/docs/screens/org-orphaned-light.webp) Each row shows: | Column | What it shows | | --- | --- | | Page | The title, plus **Private** or **In recycle bin** where they apply. | | Author | The former member's name and email, or **Former member** if their account has been deleted. | | Where | The space, or **Personal space**. | | Updated | When the page last changed. | | Actions | The **…** menu at the end of each row: **Transfer to me** and **Delete**. | If there's nothing to deal with, the view says "Nothing has been left behind." ## Taking a page over Open the **…** menu at the end of the page's row and choose **Transfer to me**. You become the author of the page, and of every page below it whose author has also gone. Pages below it written by current members keep their author. - A page in a **shared space** stays where it is. - A page in a **personal space** moves, with all the pages below it, to the organisation's default space, outside any group. When nothing is left in that personal space, it's deleted. Moving keeps the pages' links working. ## Deleting a page Open the **…** menu at the end of the page's row and choose **Delete**. - A page that isn't in the recycle bin moves there. Confirm with **Delete**. It stays in this list, marked **In recycle bin**, until it's purged or you take it over. - A page already in the recycle bin is deleted permanently. Confirm with **Delete forever**. This can't be undone. Pages in the recycle bin are purged automatically after a while. See [Recycle bin](/spaces/recycle-bin). ## In the audit log Taking over and deleting orphaned pages in shared spaces are recorded in the [audit log](/organisation/audit-log). For a private page, the entry doesn't include its title. --- # Working in several organisations One Folioso account can belong to several organisations, each with its own web address, spaces, plan and role for you; you switch between them from the sidebar. ## How it works Every account gets its own organisation when it registers. You join other organisations by invitation, or through a verified email domain. You can't create a second organisation of your own while you already belong to one. Each organisation is separate: - it has its own web address, so each one opens at a different address in your browser; - your role (admin or member) is set per organisation; - you have a personal space ("My space") in each one; - plans, seats and features belong to each organisation, not to your account. Your profile, password, two-factor authentication and preferences belong to your account and are the same everywhere. API tokens are tied to one organisation each. See [API tokens](/account/api-tokens). ## Switching organisation 1. Click the organisation card at the top of the sidebar. 2. Under **Organisations**, choose the one you want. The list shows each organisation with your role, or **suspended** if an admin there has [suspended](/organisation/suspending-members) you. The **Organisations** list only appears once you belong to more than one. When you sign in, Folioso opens the organisation you last visited. ## Joining by invitation An admin of the other organisation invites your email address. Open the link in the email: - if you're signed in with that address, you join straight away; - if you're signed in with a different account, Folioso explains the mismatch, and you can sign out and use the right account; - if you're signed out, sign in with the invited address, or create an account. Invitations expire after 7 days. Ask the admin to resend an expired one. See [Members and invitations](/organisation/members). If you registered with GitHub, every open invitation sent to your verified GitHub email address is accepted when you sign up. ## Joining through your company's email domain If an organisation has verified your email domain, and your email address is verified, Folioso offers to let you join it without an invitation. You'll see the offer: - after you sign up or sign in, on a page headed "Your team is already here"; - in the organisation switcher, as **Join** followed by the organisation's name. Click **Join** to become a member, or **Not now** to skip it. See [Verified domains](/organisation/domains). ## Leaving an organisation Click the organisation card and choose **Leave organisation**. You can't leave your only organisation, one where you're the only member, or one where you're the owner or the last admin. See [Admins and ownership](/organisation/roles-and-ownership#leaving-an-organisation). ## If you're left with no organisation If your last organisation is deleted, Folioso shows "You don't have a workspace" when you sign in. Type a **Workspace name** to create a new one and carry on. --- # Deleting an organisation Deleting an organisation closes it for everyone straight away and erases it permanently after a 30-day grace window, during which an admin can restore it from an emailed link. ## Who can delete an organisation Any organisation admin. You confirm by typing the organisation's name and your password. ## What deletion removes Once the grace window ends, Folioso permanently deletes: - every space, group, tag and team; - every page, including pages in members' personal spaces and in the recycle bin; - all memberships, invitations and settings. Members' accounts aren't deleted. They keep their other organisations. Invoices that have already been issued are kept as financial records. > [!TIP] > To leave without deleting the organisation for everyone else, [leave it](/organisation/roles-and-ownership#leaving-an-organisation) instead. Members can [export their data](/account/exporting-your-data) before you delete. ## Deleting the organisation 1. Open **Settings** and scroll to **Danger zone**. 2. Click **Delete organisation…**. 3. Read the summary: how many members lose access, and how many spaces and pages are deleted. 4. Type the organisation's name in **Type [name] to confirm**. 5. Type your password in **Confirm your password**. 6. Click **Delete organisation**. ## What happens next Straight away: - **The organisation closes for everyone.** Its address stops working and nobody can open its spaces or pages. - **Every member gets an email** saying the organisation is scheduled for deletion. The admins' copy contains a link to restore it. - **The subscription is cancelled at the end of the current billing period.** Members who have no other organisation are asked to create a new workspace when they next sign in. After 30 days, the organisation is erased permanently. Nothing can be recovered after that. ## Restoring an organisation Within the 30 days, any admin of the organisation can restore it: 1. Open the restore link in the deletion email. 2. Sign in, if you aren't already, with your admin account. The link only works for an admin of that organisation. 3. Click **Restore** followed by the organisation's name. Everything comes back exactly as it was: every space, page, team and member, and the same address. The subscription doesn't come back on its own. It stays cancelled, and runs out at the end of the period it was in. To keep your plan, resume or choose it again on the Billing tab. See [Choosing a plan](/billing/choosing-a-plan). If you decide not to restore it, just close the page. The deletion goes ahead. ## Deleting your account instead If you're the only member of an organisation and you delete your Folioso account, the organisation is deleted with it. See [Deleting your account](/account/deleting-your-account). --- # Plans Folioso has four plans: Free, Pro, Team and Business. This page explains how plans work, the difference between flat and per-seat plans, and covers trials and older plan versions. ## The four plans Every organisation is on exactly one plan, and the plan belongs to the organisation, not to you. If you belong to several organisations, each one has its own plan. Every new organisation starts on **Free**, and Free is also the plan an organisation returns to when it stops paying. **Pro**, **Team** and **Business** are paid plans that raise the limits and add features. What each plan includes, and what it costs, can change over time, so this documentation doesn't list them. See the [pricing page](https://folioso.dev/pricing) for the current terms of every plan. Organisation admins also see them on the Billing page when choosing a plan. ## What a plan controls A plan sets two kinds of terms: - **Limits**, such as how many spaces, groups and teams the organisation can create, how many pages each person can share at once, how many seats are included, the largest single image upload, the organisation's total image storage, and how long automatic version history is kept. A limit can also be unlimited. - **Features**, which a plan either includes or doesn't, such as version history, PDF export, API access, a custom organisation address, private spaces, single sign-on, member suspension, the audit log, custom branding and domain auto-join. When you reach a limit, or try to use a feature your plan doesn't include, Folioso tells you and points organisation admins to the Billing page. Each feature's page in this documentation describes what you see. A few things apply whatever your plan: - **Spaces** counts the organisation's shared spaces. Everyone's personal space ("My space") is extra and doesn't count. - **Image storage** is shared by the whole organisation. On a per-seat plan, the allowance is an amount per seat multiplied by the number of seats you're billed for. On a flat plan, it's a single amount. - **Version history retention** applies to the automatic daily snapshots. Snapshots you take by hand are kept whatever your plan. See [Version history](/editor/version-history). - **Real-time collaboration** is on every plan. It switches on wherever a page is shared or its space has more than one member. See [Editing together](/editor/collaboration). ## Flat and per-seat plans A plan is billed in one of two ways: - **Flat** plans charge one price for the organisation, and that price includes a fixed number of seats. When every seat is taken, you can't invite anyone else until you free a seat or move to a plan with more room. - **Per-seat** plans charge for each member, so there is no fixed cap: inviting someone adds a seat when they join. A per-seat plan can have a minimum number of seats, so you're billed for at least that many even with fewer members, and the spare seats are filled first as people join. The [pricing page](https://folioso.dev/pricing) shows how each plan is billed, how many seats a flat plan includes, and any minimum on a per-seat plan. See [Seats](/billing/seats) for exactly who counts and when the count changes. ## Free trials A plan can come with a free trial. When it does, the plan chooser shows its length on the plan (such as "*N*-day free trial.") and, at registration, "You won't be billed until the trial ends." During a trial nothing is billed. When the trial ends, the organisation moves on to the paid plan automatically, and the trial's end date becomes your renewal date from then on. If you return to Free before the trial ends, the trial simply ends there and nothing is charged. ## Older plan versions Folioso sometimes updates a plan's terms by publishing a new version of it. Existing customers aren't moved. If you're on an earlier version, you keep its price and limits for as long as you stay on it, and the Billing page marks your plan with a **Legacy terms** badge and explains which version you're on. Only the current version of each plan is sold, so any plan change from a legacy version moves you onto current terms for good. Before that happens, Folioso shows a separate **Move off your legacy plan?** screen comparing your price and features **Now** and **After**, with a list of **What changes**. Choose **Move to** *plan* to confirm, or **Back** to keep your legacy terms. > [!IMPORTANT] > Moving off a legacy version is permanent. Your old terms can't be restored afterwards, even by choosing the same plan again. Moving to Free from a legacy version doesn't need this confirmation, but it still ends your legacy terms when the move lands. ## Who can change the plan Only organisation admins can see the Billing page and change the plan. Members don't see it at all. See [Choosing a plan](/billing/choosing-a-plan). --- # Seats A seat is a place for one member in your organisation. This page explains who takes a seat, how adding and removing people changes your seat count and bill, and what happens when a plan has no seats left. ## What counts as a seat Every member of the organisation takes one seat, admins included. Two exceptions: - **Suspended members don't take a seat.** Suspending someone frees their seat, and unsuspending them takes it back. See [Suspending members](/organisation/suspending-members). - **Members who have asked to delete their account still take a seat** until the deletion is final, because they can change their mind and reactivate their account during the grace window. On a flat plan, **pending invitations also count** towards the seat limit while they're open, so you can't send more invitations than you have seats. An invitation stops counting when it's accepted, revoked or expires. On a per-seat plan, invitations don't count; the seat is added when the person joins. ## Seats on flat plans A flat plan includes a fixed number of seats. The price doesn't change with the number of members, but you can't go over the number of seats. The [pricing page](https://folioso.dev/pricing) shows how many seats each flat plan includes. The **Members** page shows how many seats are in use, for example "*N* of *M* seats used". When every seat is taken: - the invite form is disabled and its button reads **Upgrade plan**, which takes you to the Billing page; - the page explains "Every seat on your plan is taken." with a link to upgrade. To add more people, free a seat (remove a member or revoke a pending invitation) or move to a per-seat plan. See [Upgrading](/billing/upgrading). ## Seats on per-seat plans On a per-seat plan you're billed for each seat, and there's no fixed cap: inviting someone simply adds a seat when they join. The Billing page shows the number of seats you're billed for, such as "5 seats, billed per seat — inviting a member adds one." If you're billed for more seats than you have members, for example while a removal is waiting for the next period, it reads "… inviting a member fills a spare seat before adding one." ![The Billing page of an organisation on a per-seat plan, with its Seats meter and a seat decrease waiting for the next period](/images/docs/screens/billing-current-light.webp) ### Minimum seats A per-seat plan can have a minimum number of seats. You're billed for at least that many, even with fewer members, and new members fill those spare seats first at no extra charge. The Billing page tells you when this applies: "*N* seats, billed per seat (the plan minimum) — inviting a member adds one once all *N* are in use." The [pricing page](https://folioso.dev/pricing) shows each plan's minimum. The minimum always applies: your seat count never drops below it, whether members leave or are suspended. ## Adding members On a per-seat plan, a seat is added the moment someone takes one: - an invited person accepts their invitation; - someone joins through a verified domain (see [Verified domains](/organisation/domains)); - an admin unsuspends a member. The extra seat applies **immediately** and is charged **pro rata**: you pay for it from that day to the end of the current billing period, then at the full rate from the next period. Your renewal date doesn't change. In the **Billing history** on the Billing page, the period is split at that day, and the shorter part is marked **Prorated**. If a seat is still free under the plan minimum, nothing extra is charged. ## Removing members On a per-seat plan, removing a member, or a member leaving, **lowers the seat count at your next renewal**, not straight away. You've already been billed for the current period, so you keep that seat until the period ends, and nothing is refunded mid-period. Suspending a member works the same way. While a decrease is waiting, the Billing page shows both numbers, for example "6 seats, 5 from next period." If someone joins before the renewal, the scheduled decrease is adjusted to match. At renewal Folioso also makes sure you're never billed for fewer seats than you have members, or than the plan minimum. On a flat plan, removing a member frees their seat immediately, and the price doesn't change. ## When there are no free seats Some actions need a free seat and are refused on a flat plan that's full: - **Sending an invitation.** See [Seats on flat plans](#seats-on-flat-plans) above. - **Accepting an invitation.** If the plan filled up after the invitation was sent, the invitee sees "We couldn't add you to *organisation*: it has no free seats. Ask an admin there to free a seat, then open the invitation link again." - **Unsuspending a member.** The member stays suspended until a seat is free. A flat plan can occasionally end up with more members than seats, for example when someone reactivates their account during the deletion grace window. The Billing page then warns that the organisation has more members than seats and asks you to upgrade or remove a member. ## Moving between plans When you change plan, your seats follow the new plan's rules: - Moving to a **per-seat plan** bills one seat per member, never below the plan's minimum. - Moving to a paid **flat plan** while you have more members than it includes is refused, with a message asking you to choose a seat-based plan. Remove or suspend members first, or pick a per-seat plan. - **Returning to Free** is allowed with any number of members, but members beyond the seat limit are suspended when the move lands. See [What happens to members over the seat limit](/billing/downgrading#what-happens-to-members-over-the-seat-limit). --- # Choosing a plan You pick your organisation's plan when you register, and an admin can change it at any time from the Billing page. This page covers both, and who is allowed to do it. ## Who can choose a plan Only **organisation admins** can choose or change the plan, see the Billing page, update billing details or download invoices. Members don't see the Billing page at all; the read-only summary under **Plan & Billing** in their account settings shows the current plan and usage. When you register, you're the admin of the new organisation that comes with your account, so you can pick its plan straight away. ## At registration After you create your account, step 2 of registration is **Pick a plan**. ![The plan step of registration](/images/docs/screens/choose-plan-light.webp) 1. Choose a plan card. **Free** is selected to begin with. 2. If any plan is offered both monthly and annually, a **Monthly** / **Annual** switch appears above the cards. 3. Select **Continue to Folioso**. Each card shows the plan's price, its minimum seats if it has one (for example "Minimum *N* seats."), any free trial, and what it includes. On a per-seat plan the price shown is for the number of seats you'd be billed for. Choosing Free sets nothing up: the organisation is already on it. Choosing a paid plan starts it straight away, with your first billing period running from today. As the page says, "Paid plans activate immediately — card details come later when billing goes live." See [Invoices](/billing/invoices) for how you're billed in the meantime. If a paid plan can't be applied (for example it wouldn't fit the organisation's members), you still get into Folioso on Free, and can choose again from the Billing page. > [!NOTE] > You won't see this step if you registered through an invitation to an existing organisation, or through a signup invite from Folioso. An existing organisation already has a plan, and a signup invite comes with its plan already chosen. ### If your email matches a verified domain If your email address belongs to a domain an organisation has verified, the step also offers to join that organisation, with "Or pick a plan for your own workspace below." underneath. See [Verified domains](/organisation/domains). ### When paid plans are invite-only Folioso sometimes opens paid plans by invitation only. While it does, paid plans are marked **Invite only** and can't be selected, and the page explains: "Paid plans are invite-only for now. Start on Free, and register your interest to hear when they open." Select **Register interest** to be told when they open. ## On the Billing page To open it, go to your organisation's admin menu and choose **Billing**, or select **Manage plan** under **Plan & Billing** in your account settings. The top of the page shows: - **Current plan**, with a summary of your main limits; - your renewal date, or "You're on the Free plan."; - any change scheduled for the end of the period; - a **Seats** meter showing how many members you have against your seats. To change plan: 1. Select **Upgrade** (on Free) or **Change plan** (on a paid plan). The **Change plan** chooser opens. 2. Pick a plan in the chooser. Your current plan is labelled "Your current plan". Each card shows the plan's price for your number of seats and what it includes. 3. Select **Subscribe**, **Change plan** or, if you picked Free, **Return to Free**. The change is made straight away, with no separate confirmation. The exception is a legacy plan version: there the button reads **Review change** and takes you to a confirmation screen first (see [Older plan versions](/billing/plans#older-plan-versions)). ![The Change plan chooser opened from Upgrade on a Free organisation, with Free labelled as your current plan](/images/docs/screens/billing-free-light.webp) ![The Change plan chooser with the Team plan selected, showing its price for the organisation's seats and what it includes](/images/docs/screens/billing-change-plan-light.webp) What happens next depends on the move: | Move | When it applies | Billing | | --- | --- | --- | | Free to a paid plan | Immediately | A new billing period starts today | | To a plan that costs more | Immediately | The rest of this period is charged at the new price ([Upgrading](/billing/upgrading)) | | To a plan that costs less | At the end of the period | You keep your current plan until then ([Downgrading](/billing/downgrading)) | | To a plan that costs the same | Immediately | No change to what you pay | | To Free | At the end of the period | Nothing more is charged after that ([Moving to the Free plan](/billing/returning-to-free)) | "Costs more" and "costs less" compare what the whole organisation pays, the price times the number of seats, not the plan's name. For example, moving from a flat plan to a per-seat plan with a minimum can count as an upgrade because of the seats it bills. A message confirms what happened, such as "You're now on *plan*. This period's charge was prorated; your renewal date is unchanged." or "Your change to *plan* is scheduled for *date* — your current plan continues until then." ### When moving to a paid plan is paused While paid plans are invite-only, the **Upgrade** and **Change plan** buttons are replaced by "Moving to a paid plan is paused for now. Your current plan carries on as normal." You can still return to Free, cancel a scheduled change, or keep your plan after scheduling a return to Free. ## Currency All plans are priced in one currency, which you can see on the [pricing page](https://folioso.dev/pricing). A plan change always stays in the currency you're billed in. --- # Upgrading Moving to a plan that costs more takes effect straight away, and you pay the difference only for what's left of the current billing period. This page explains how to upgrade, how the charge is worked out and what changes immediately. ## What counts as an upgrade A move is an upgrade when the organisation would pay more on the new plan than on the current one. Folioso compares the total, the price times the number of seats, rather than the plan names. So: - Free to any paid plan is an upgrade. - Moving from a flat plan to a per-seat plan is normally an upgrade, but it depends on the number of seats you'd be billed for, including any minimum. A move that costs the same also applies immediately, with no change to what you pay. A move that costs less is a downgrade, and waits for the end of the period: see [Downgrading](/billing/downgrading). For prices, see the [pricing page](https://folioso.dev/pricing). ## How to upgrade You need to be an organisation admin. 1. Open **Billing** from your organisation's admin menu. 2. Select **Upgrade** (on Free) or **Change plan**. 3. Pick the new plan. If both monthly and annual prices are offered, choose with the **Monthly** / **Annual** switch. 4. Select **Subscribe** or **Change plan**. You'll see "You're now on *plan*. This period's charge was prorated; your renewal date is unchanged." If you're on a legacy version of your plan, you'll be asked to confirm the move first, because it can't be undone. See [Older plan versions](/billing/plans#older-plan-versions). ## How the charge is worked out When you upgrade part-way through a billing period, the period is split on the day you upgrade: - the days already gone are charged at your old plan's price; - the days remaining are charged at the new plan's price. Your renewal date stays the same, and the next full period is billed at the new price. Both parts appear in the **Billing history** on the Billing page, and the split rows are marked **Prorated**. They're invoiced together when the period closes. See [Invoices](/billing/invoices). **Upgrading from Free** works differently, because there's nothing to split: your first paid billing period starts on the day you upgrade, and that day becomes your renewal date. On a per-seat plan you're billed for one seat per member, never below the plan's minimum. See [Seats](/billing/seats). ## What changes straight away Everything the new plan includes is available as soon as the change is confirmed. For example: - higher limits on spaces, groups, teams and shared pages, and larger uploads; - more seats, so you can invite people again; - features such as version history, PDF export, the API, private spaces, single sign-on, the audit log, custom branding or domain auto-join, if the new plan includes them. See [Plans](/billing/plans#what-a-plan-controls) for the kinds of limits and features a plan sets, and the [pricing page](https://folioso.dev/pricing) for what each plan includes. ## An upgrade replaces a scheduled change There's only ever one plan change waiting. If you've scheduled a downgrade, choosing a plan that costs more replaces it and applies straight away. If you've scheduled a return to Free, the Billing page offers **Keep my plan** instead of **Change plan**. Select **Keep my plan** first, then upgrade. ## Special rates If Folioso has agreed a special rate with your organisation, including a free place on a plan through a signup invite, it applies only to the plan it was agreed for. Choosing a different plan yourself ends it, and the new plan is billed at its normal price. ## Getting suspended members back If members were suspended because a plan didn't have enough seats for them, upgrading doesn't bring them back on its own. After upgrading, open **Members** and choose **Unsuspend** from the **…** menu on each person's row. On a per-seat plan each unsuspended member adds a seat, charged pro rata. See [Suspending members](/organisation/suspending-members). --- # Downgrading Moving to a plan that costs less is scheduled for the end of your current billing period, so you keep what you've paid for until then. This page covers scheduling and cancelling a downgrade, the seat rules, what happens to members over the new plan's seat limit, and which features stop. ## When a downgrade applies A move is a downgrade when the organisation would pay less on the new plan, comparing the price times the number of seats, not the plan names. For moving to Free, see [Moving to the Free plan](/billing/returning-to-free), which works the same way with a few extras. A downgrade never happens mid-period. It's scheduled for the end of the current billing period, your renewal date. Until then: - you stay on your current plan, with all its features and limits; - you keep paying your current price for the current period, and nothing is refunded; - the new plan's price applies from the next period. ## How to downgrade You need to be an organisation admin. 1. Open **Billing** from your organisation's admin menu. 2. Select **Change plan** and pick the plan you want. 3. Select **Change plan**. You'll see "Your change to *plan* is scheduled for *date* — your current plan continues until then." The Billing page then shows "Changes to *plan* on *date*." ![The Billing page announcing a scheduled change to another plan on the renewal date, with Cancel scheduled change next to it](/images/docs/screens/billing-scheduled-change-light.webp) There's only one scheduled change at a time. Choosing another plan replaces the one you scheduled, and choosing Free replaces it with a return to Free. ## Cancelling a scheduled downgrade Changed your mind? Select **Cancel scheduled change** next to the announcement on the Billing page. You'll see "Your scheduled plan change has been cancelled — you stay on your current plan." Nothing else changes, and any special rate you had agreed with Folioso is kept. You can cancel any time before the change lands. ## Seat limits when downgrading The new plan has to be able to seat your members: - **Onto a paid flat plan**: if you have more members than the plan includes, the change is refused with "This plan allows *N* seat(s) but the organisation has *M* member(s) — choose a seat-based plan." Remove members, or suspend them while you're still on a per-seat plan, then try again. This check applies whether the change would happen now or at the end of the period. - **Onto a per-seat plan**: you're billed for one seat per member from the next period, never below the plan's minimum. - **Onto Free**: always allowed, but members beyond Free's seats are suspended when the move lands. See below. ## What happens to members over the seat limit A scheduled move onto a paid flat plan is checked when you schedule it, but people can still join before it lands (for example into a spare seat under your current plan's minimum). A return to Free can land with more members than Free includes. Either way, when the move lands on a plan without enough seats, Folioso suspends members until the organisation fits, in this order: 1. The **owner** is always kept. 2. The **admin who chose the move** is kept next, while there are seats left. 3. **Members who aren't admins** are suspended, newest to join first. 4. If that isn't enough, the **least recently active admins** are changed to members and then suspended. Everyone affected is emailed: - each suspended member is told their account in the organisation has been suspended and their pages are kept; - the remaining admins get one email listing who was suspended, and which of them were admins. Suspended members keep their account and their pages, stay in any other organisation they belong to, and don't take a seat. They see a page saying their account is suspended and asking them to contact an admin. ### Getting those members back 1. Move to a plan with enough seats, such as a per-seat plan. See [Upgrading](/billing/upgrading). 2. Open **Members** and, for each person, choose **Unsuspend** from the **…** menu at the end of their row. Admins who were changed to members come back as members. Change their role back on the **Members** page afterwards. See [Suspending members](/organisation/suspending-members) and [Admins and ownership](/organisation/roles-and-ownership). ## What you lose on a smaller plan When the downgrade lands, the new plan's limits and features apply. **Nothing you've created is deleted**, with one exception for old automatic snapshots (below), but some features stop working: | Feature | What happens after the downgrade | | --- | --- | | Spaces, groups, teams, shared pages | Everything you have stays. You can't create more until you're under the new limit. | | Uploads and image storage | Existing images stay. New uploads must fit the new size and storage limits. | | Private spaces | Existing private spaces stay private. You can't create a private space or make a space private. | | Custom branding | Your logo and colours stop appearing on shared pages straight away. | | Custom organisation address | Your address stays as it is, but you can't change it again. | | Single sign-on | Members can no longer sign in through your identity provider. They sign in with their password or GitHub instead. | | API access | API requests with your members' tokens are refused. | | Audit log | Security events are still recorded, but you can't view or export the log until you upgrade again. | | Domain auto-join | People can no longer join through your verified domains, and you can't add domains. | | Member suspension | You can't suspend members, and automatic suspension stops. You can still unsuspend members and turn automatic suspension off. | | PDF export | Pages can't be exported as PDF. Markdown export still works. | | Version history | On a plan without version history you can't browse, take or restore snapshots, and daily snapshots stop. | > [!WARNING] > Each plan sets how long automatic snapshots are kept, and smaller plans usually keep them for less time. A plan without version history still keeps existing snapshots for its retention period, even though you can't open them there. After a downgrade, automatic daily snapshots older than the new plan's limit are deleted, though each page keeps at least its most recent snapshot. Snapshots you took by hand are never deleted this way. See [Version history](/editor/version-history). For what each plan includes, see the [pricing page](https://folioso.dev/pricing). --- # Moving to the Free plan You can move a paid organisation back to Free at any time. The move is scheduled for the end of your billing period, you can call it off until then, and nothing is deleted. This page explains how it works and what to expect when it lands. ## Before you start - Only organisation admins can do this. - Free includes a fixed number of seats. If your organisation has more members than that, the excess members are suspended when the move lands. Read [Members over the seat limit](#members-over-the-seat-limit) first. - Free usually has smaller limits and fewer features. Your content is kept, but anything Free doesn't include stops. Check the [pricing page](https://folioso.dev/pricing) for what Free includes. See [What you lose on a smaller plan](/billing/downgrading#what-you-lose-on-a-smaller-plan). ## How to move to Free 1. Open **Billing** from your organisation's admin menu. 2. Select **Return to Free**. 3. A confirmation appears on the Billing page, under your plan. Read it: "Return to the Free plan on *date*? You keep everything you're paying for until then, and anything above the Free limits stops being available afterwards." 4. Select **Return to Free** to confirm, or **Keep plan** to back out. ![The confirmation strip on the Billing page, with Return to Free and Keep plan](/images/docs/screens/billing-return-to-free-light.webp) You can also pick **Free** in the **Change plan** chooser and select **Return to Free**. It does exactly the same thing. You'll see "Your organisation returns to the Free plan when the current period closes." The Billing page then shows "Returns to the Free plan on *date* — everything above the Free limits stops being available then." A return to Free replaces any downgrade you had already scheduled. ## Until the end of the period Nothing changes until your renewal date. You keep your plan, its features and your seats, and you're not charged anything extra. The current period is invoiced as usual when it closes, and that's your last charge. See [Invoices](/billing/invoices). ## Keeping your plan after all Select **Keep my plan** on the Billing page any time before the date. You'll see "Your plan will carry on as normal — the return to Free has been called off." Your plan then renews as normal. If you'd scheduled a downgrade before choosing Free, it isn't restored: you stay on your current plan, and can schedule the downgrade again from **Change plan**. > [!NOTE] > If moving to paid plans is paused when you schedule the return, the confirmation warns "Paid plans are paused right now, so you won't be able to move back onto this one." Once the return lands, you'd stay on Free until paid plans open again. **Keep my plan** still works up to the date. ## When the move lands On your renewal date the organisation moves to Free: - the seat count becomes Free's, and members over it are suspended (see below); - the Free limits apply, and features Free doesn't include stop; - nothing more is charged. Free is never invoiced. ### Members over the seat limit When the move lands, Folioso suspends members until the organisation fits Free's seats, in this order: 1. The **owner** is always kept. 2. The **admin who scheduled the move** is kept next, while there are seats left. 3. **Members who aren't admins** are suspended, newest to join first. 4. If that isn't enough, the **least recently active admins** are changed to members and then suspended. Each suspended member is emailed, and the remaining admins get one email listing who was suspended. Suspended members keep their account and pages, and stay in any other organisations they belong to. To bring people back, move to a plan with enough seats and then choose **Unsuspend** from the **…** menu on each of their rows on the **Members** page. See [Getting those members back](/billing/downgrading#getting-those-members-back). > [!TIP] > If you want to pay less while keeping your whole team, check how many seats Free includes on the [pricing page](https://folioso.dev/pricing) first. A smaller paid plan may suit you better: see [Plans](/billing/plans). ## Special rates and legacy terms - **A special rate** agreed with Folioso isn't deleted when you move to Free, so Folioso can restore it if you come back. If you choose a paid plan again yourself, it's billed at the normal price. - **Legacy plan terms** end when the move lands. Coming back later means the current version of the plan. See [Older plan versions](/billing/plans#older-plan-versions). ## Moving to Free with unpaid invoices You can return to Free while an invoice is unpaid or overdue. The move lands on the same date as usual, but the money is still owed: moving to Free doesn't cancel an invoice. See [Overdue payments](/billing/overdue-payments). ## Moves that happen straight away Two things can move an organisation to Free immediately rather than at the end of the period: - an invoice left unpaid past the grace period (see [Overdue payments](/billing/overdue-payments)); - Folioso support making the change for you. The current period is then cut short at that day, and you're invoiced only for the days you used. The same seat rules apply. --- # Invoices Folioso issues an invoice for each paid billing period once it has closed. This page explains when invoices are issued, what they contain, how to download them and what each status means. ## When invoices are issued You're billed in arrears: an invoice covers billing periods that have already ended. - Each night, Folioso issues an invoice for every billing period that closed and had something to charge. - One invoice covers everything that's due for the organisation at that point, so a period split by an upgrade or an added seat appears as several lines on one invoice. - Periods that cost nothing, such as time on Free or on a free-of-charge rate, are never invoiced. - Each invoice is **due 14 days** after it's issued. So on a monthly plan you'll normally get one invoice shortly after each renewal date, covering the month that has just ended. An invoice is also issued for the final days of a paid plan when an organisation moves to Free straight away. When an invoice is issued, Folioso emails it to the billing email on the invoice (or the organisation owner if none was set). You'll also find it on the Billing page. Folioso emails reminders about overdue invoices too: see [Overdue payments](/billing/overdue-payments). ## Finding your invoices You need to be an organisation admin. 1. Open **Billing** from your organisation's admin menu. 2. Scroll to **Invoices**. ![The Invoices table on the Billing page, with Billing history below it](/images/docs/screens/billing-invoices-light.webp) Each row shows the **Reference**, the **Issued** and **Due** dates, the **Total**, how much has been **Paid** so far, and the **Status**. Until your first billing period closes, the section reads "No invoices yet — one is issued when your first billing period closes." Below the invoices, **Billing history** lists the charge for each billing period: the period, plan, number of seats (with any change from the period before) and amount, marked **Prorated** when the period was split. It's a running estimate of what each period costs. The invoice is the final record of what you were asked to pay. ## Downloading an invoice Select **PDF** on the invoice's row. The file is named after the invoice reference, for example `INV-2026-000123.pdf`. ![An invoice document showing Billed to, the issue and due dates, one line per billing period and the total](/images/docs/screens/invoice-light.webp) The invoice shows: - the reference, such as `INV-2026-000123`. Numbers run in sequence within each year; - **Billed to**: the name, email, address, country and VAT number from your [billing details](/billing/billing-details) at the time it was issued; - the **Issued** and **Due** dates; - one line per billing period, with a **Description** (the plan name and the period's dates), **Qty** (seats), **Unit** price and **Amount**; - the **Subtotal** and **Total**; - the period the invoice covers. The PDF doesn't show whether the invoice has been paid, because the document doesn't change once it's issued. Check the **Status** on the Billing page for that. ## Statuses | Status | Meaning | | --- | --- | | **Issued** | The invoice has been raised and isn't due yet. | | **Paid** | Payments covering the full total have been received. | | **Overdue** | The due date has passed and the invoice hasn't been paid in full. See [Overdue payments](/billing/overdue-payments). | | **Void** | Folioso has cancelled the invoice. Nothing is owed on it. | A part payment shows in the **Paid** column, but the status stays **Issued** or **Overdue** until the full total has been received. ## Invoices can't be changed Once issued, an invoice is a permanent record. It isn't edited afterwards: - Changing your billing details only affects invoices issued after the change. - If an invoice is wrong, Folioso voids it rather than editing it. - Invoices stay available even if you move to Free. ## Paying an invoice There's no card payment in Folioso yet. As the Billing page says, invoices are "Payable offline for now." When Folioso receives your payment, it's recorded against the invoice and the status changes to **Paid**. ## Currency Invoices are in the currency your plan is priced in. Every plan uses the same single currency, shown on the [pricing page](https://folioso.dev/pricing). --- # Billing details Billing details say who your invoices are addressed to and where payment reminders go. This page explains each field and when changes take effect. ## Who can change them Only organisation admins. The **Billing details** section is on the Billing page, so it's hidden from members along with the rest of billing. Fill them in before your first invoice is issued: an invoice copies the details at the moment it's issued, and can't be changed afterwards. ## Updating your billing details 1. Open **Billing** from your organisation's admin menu. 2. Find **Billing details**. 3. Fill in the fields you need. All of them are optional. 4. Select **Save billing details**. You'll see "Billing details saved." ![The billing details form](/images/docs/screens/billing-details-light.webp) ## The fields | Field | What it's for | Shown on the invoice as | | --- | --- | --- | | **Legal name** | The name of the person or company being billed, up to 255 characters. | The first line under **Billed to**. If it's empty, your organisation's name is used. | | **Billing email** | Where invoices and payment reminders are sent, up to 255 characters. | The email under **Billed to**. If it's empty, the organisation owner's email is used. | | **Address** | Your postal address, up to 1,000 characters. Line breaks are kept. | The address under **Billed to**. | | **Country** | A two-letter country code, such as `GB` or `US`. | The country under **Billed to**. | | **VAT number** | Your VAT registration, up to 64 characters. | "VAT" followed by the number. | The VAT number is "Recorded on your invoices. It is not verified." Folioso doesn't check it, and doesn't add VAT or any other tax to invoices. ## When changes take effect Changes apply to **future invoices only**. Invoices already issued keep the details they were issued with, because an invoice is a fixed record. See [Invoices](/billing/invoices#invoices-cant-be-changed). The billing email works the same way: an invoice and the reminders about it go to the email on that invoice, which is the billing email at the time it was issued, or the owner's email if none was set. If you change the billing email, reminders for invoices already issued still go to the old address. > [!TIP] > If you don't set a billing email, invoices and reminders go to the organisation owner. Set one if someone else, such as a finance team, should receive them. To change the owner, see [Admins and ownership](/organisation/roles-and-ownership). --- # Overdue payments An invoice becomes overdue when its due date passes without full payment. Folioso then sends reminders, and if the invoice is still unpaid after a grace period, the organisation is moved to Free. This page explains the timeline, what you keep along the way and how to settle up. ## How payment works today There's no card payment in Folioso yet: invoices are payable offline. When Folioso receives a payment, it's recorded against the invoice, and once the payments cover the full total the invoice shows **Paid**. A part payment appears in the **Paid** column on the Billing page, but the invoice stays overdue until it's paid in full. ## The timeline Each invoice is due 14 days after it's issued. If it's still unpaid on the due date: | When | What happens | | --- | --- | | Due date | The invoice's status changes to **Overdue**. | | 1 day after | First reminder email. | | 7 days after | Second reminder email. | | 14 days after | Third reminder email. | | 21 days after | Grace period ends. If the invoice is still unpaid, the organisation moves to Free straight away. | Folioso checks once a night, so each step happens on the night it falls due. There's no banner or warning at the top of the Billing page. To see which invoices are overdue, look at the **Status** column under **Invoices**. ![The Invoices table on the Billing page, with one invoice marked Overdue and an earlier one Paid](/images/docs/screens/billing-overdue-light.webp) ## Reminder emails Each reminder says "Invoice *reference* is overdue", with the amount and due date, and that the organisation returns to the free plan if it stays unpaid. It links to your Billing page. Reminders go to the billing email on the invoice. If no billing email was set when the invoice was issued, they go to the organisation owner. See [Billing details](/billing/billing-details). If you've already paid, you can ignore the reminder. Reminders stop as soon as the invoice is paid or voided. ## During the grace period While an invoice is overdue, your organisation keeps its plan, with every feature, limit and seat. Nothing is switched off before the grace period ends. If you have several overdue invoices, the grace period runs from the oldest one, so a newer invoice doesn't give you extra time. ## When the grace period ends If the oldest overdue invoice is still unpaid 21 days after its due date, Folioso moves the organisation to Free immediately: - the current billing period is cut short that day, and a final invoice is issued for the days used on the paid plan; - Free's limits apply and paid features stop. See [What you lose on a smaller plan](/billing/downgrading#what-you-lose-on-a-smaller-plan); - members beyond the seats Free includes are suspended, keeping the owner. See [Members over the seat limit](/billing/returning-to-free#members-over-the-seat-limit). Your content isn't deleted. > [!IMPORTANT] > Moving to Free doesn't cancel what you owe. Overdue invoices stay on the Billing page and remain payable in full. ## Coming back to a paid plan You can choose a paid plan again from the Billing page at any time. The invoices from before the move are still owed, but they won't move you to Free a second time. Any new invoice that goes overdue follows the same timeline from the start. To bring suspended members back after upgrading, choose **Unsuspend** from the **…** menu on each of their rows on the **Members** page. ## Moving to Free yourself You can schedule a return to Free while an invoice is overdue. It lands at the end of your period as usual. The invoice remains owed. See [Moving to the Free plan](/billing/returning-to-free). --- # Signing in Sign in to Folioso with your email and password, with GitHub, or through your organisation's single sign-on, and keep your account secure with email verification and two-factor authentication. ![The sign-in page](/images/docs/screens/login-light.webp) ## Sign in with your email and password 1. Go to the sign-in page and enter your **Email** and **Password**. 2. Tick **Remember me** if you want to stay signed in on this device after the browser closes. 3. Select **Sign in**. If your account has two-factor authentication turned on, you're asked for a code next. See [The two-factor code prompt](#the-two-factor-code-prompt). After several failed attempts in a short time, sign-in is paused for a minute. Wait and try again. ## Sign in with GitHub Select **Continue with GitHub** on the sign-in page and approve Folioso on GitHub. - **If your GitHub account is already linked** to a Folioso account, you're signed straight in. - **If you already have a Folioso account with the same email address**, GitHub is linked to it, but only when the address is verified on both sides. GitHub only shares verified addresses. If your Folioso address isn't verified yet, the link is refused: sign in with your password, verify your email, then try GitHub again. - **If there's no matching account**, a new Folioso account is created with your GitHub email already verified, and you choose a plan next. While Folioso is taking sign-ups by invitation only, this needs an invitation. GitHub needs at least one verified email address on your GitHub account. If it has none, Folioso can't sign you in. GitHub sign-in always remembers your device, and it doesn't ask for a two-factor code. Your GitHub account is the second factor in that case. > [!TIP] > An account created with GitHub has no password you know. To sign in with your email address too, open **Settings** and use **Set a password**. See [Profile and email](/account/profile). ## Sign in with single sign-on If your organisation has set up single sign-on (SSO), you can sign in through its identity provider. 1. On the sign-in page, select **Single sign-on**. 2. Under **Workspace address**, type your organisation's address (the part before `.folioso.dev`). 3. Select **Continue** and sign in at your identity provider. Single sign-on only signs in people who are already members of the organisation. It never creates an account. The first time, Folioso links your identity to the member whose verified email matches the one your provider sends. If your Folioso address isn't verified yet, sign in with your password once to verify it, then use SSO. Like GitHub, single sign-on doesn't ask for a Folioso two-factor code. Admins set it up in [Single sign-on](/organisation/single-sign-on). ## The two-factor code prompt ![The two-factor code prompt](/images/docs/screens/two-factor-challenge-light.webp) If you've turned on [two-factor authentication](/account/two-factor-authentication), signing in with a password takes two steps: 1. Enter your email and password as usual. 2. On the **Two-factor authentication** page, enter the six-digit **Authentication code** from your authenticator app and select **Verify**. Lost your phone? Select **Lost your device? Use a recovery code** and enter one of the recovery codes you saved. Each recovery code works once. ## Forgot your password 1. On the sign-in page, select **Forgot password?**. 2. Enter your email address and select **Send reset link**. 3. Open the link in the email within 60 minutes. Enter your email and a new password, then select **Reset password**. 4. Sign in with your new password. The reset link doesn't sign you in by itself. A new password needs at least 8 characters, with upper- and lower-case letters, a number and a symbol. ## Verify your email address When you register with an email and password, Folioso sends you a verification link. Open it to confirm the address. The link is valid for 60 minutes. You can use Folioso before you verify, but these actions are locked until you do: - sharing a page with someone and turning on a page's public link; - inviting people to an organisation; - adding a domain or sending a domain verification email; - joining an organisation through a verified domain. To get a fresh link, select **Verify now** in the reminder at the top of the app, then **Resend verification email**. If you change your email address later, the new address has to be verified again. See [Profile and email](/account/profile). ## Signing out Select the **Sign out** icon next to your name at the bottom of the sidebar. There's no single "sign out everywhere" button. To end your sessions on every other device, change your password: Folioso signs out all your other devices and forgets any device you'd asked it to remember. See [Profile and email](/account/profile#change-your-password). ## If your account is scheduled for deletion If you asked to delete your account and are still inside the grace window, signing in with the right password (or with GitHub) doesn't open the app. Folioso emails you a reactivation link instead. See [Deleting your account](/account/deleting-your-account). --- # Profile and email Change your name, email address, profile photo and password from the Profile section of your account settings. To open your settings, select the **Settings** icon next to your name at the bottom of the sidebar. ![Account settings showing the profile section](/images/docs/screens/settings-profile-light.webp) Your profile belongs to your account, not to one organisation. A change here shows in every organisation you belong to. ## Change your profile photo 1. In **Profile**, select **Upload photo** (or **Change photo** if you already have one). 2. Pick a PNG, JPEG, GIF, WebP or AVIF image. The largest file you can upload depends on your organisation's plan, and the limit is shown under the button. Your photo is shown as a circle. Until you upload one, Folioso shows your initials. To go back to your initials, select **Remove**. You can always remove a photo, even if your plan no longer allows uploads. ## Change your name 1. Under **Name and email**, edit **Name** (up to 100 characters). 2. Select **Save profile**. ## Change your email address 1. Under **Name and email**, enter the new address in **Email**. 2. If your account has a password, a **Current password** field appears. Enter your password. 3. Select **Save profile**. What happens next: - Folioso emails a verification link to the **new** address. Until you open it, the new address is unverified, and the actions that need a verified email (sharing, inviting people, domain features) are locked. See [Signing in](/account/signing-in#verify-your-email-address). - Folioso emails your **previous** address to say it was changed, so you'll know if someone else did it. - The change is recorded in the audit log of every organisation you belong to. Email addresses aren't case-sensitive, and each address can belong to only one account. If the address is already in use you'll see **That email address is already in use.** ## Change your password 1. Under **Change password**, enter your **Current password**. 2. Enter a **New password** and type it again in **Confirm new password**. 3. Select **Change password**. A password needs at least 8 characters, with upper- and lower-case letters, a number and a symbol, and it must differ from your current one. Changing your password: - signs out all your other devices, and forgets any device you'd asked Folioso to remember; - keeps you signed in on the device you used; - sends you an email confirming the change; - is recorded in the audit log of every organisation you belong to. ### Set a password for a GitHub account If you signed up with GitHub, you don't have a password yet. The section is called **Set a password**: enter a **New password**, confirm it, and select **Set a password**. You can then sign in with your email address as well as GitHub. You'll need a password to turn on two-factor authentication and to delete your account. > [!NOTE] > If you forget your password, use **Forgot password?** on the sign-in page. See [Signing in](/account/signing-in#forgot-your-password). --- # Preferences Choose a colour theme and set the defaults Folioso's editor uses for code blocks. Your preferences are saved to your account and follow you to every device. Open **Settings** (the icon next to your name at the bottom of the sidebar) and scroll to **Editor Preferences**. ![The editor preferences section](/images/docs/screens/settings-preferences-light.webp) ## Theme Under **Theme**, choose: | Option | What it does | | --- | --- | | **Light** | Always use the light theme. | | **Dark** | Always use the dark theme. This is the default. | | **System** | Follow your device's light or dark setting, and switch when it does. | ## Code block defaults These settings apply to [code blocks](/editor/code-blocks) you add in the editor. | Setting | What it does | Default | | --- | --- | --- | | **Default Code Language** | The language that's pre-selected when you insert a new code block. You can still change it per block. | Plain text | | **Tab Size** | How many spaces a tab inserts: **2** or **4**. | 4 | | **Line Numbers** | Whether code blocks show line numbers by default. | On | ## Saving There's no save button. Changes are saved automatically a moment after you make them, and the page header shows when the save is in progress and when it's done. Because preferences are stored with your account, they're the same in every organisation you belong to. --- # Two-factor authentication Two-factor authentication asks for a six-digit code from an authenticator app whenever you sign in with your password. It's optional for most people and required for admins and owners of an organisation with other members. ![The Security section of account settings, with two-factor authentication turned on](/images/docs/screens/settings-security-light.webp) ## When two-factor is required You must use two-factor authentication if you're an **admin** or the **owner** of an organisation that has more than one member. While the requirement applies and you haven't set it up: - the only page Folioso opens is the two-factor setup page, which says *"You administer an organisation with other members, so a second factor is required before the rest of Folioso opens."*; - your [API tokens](/account/api-tokens) stop working until you finish setting it up; - you can't turn two-factor off afterwards. The page says it's **Required for admins and owners of an organisation with other members, so it cannot be switched off.** If you're only a member, or every organisation you administer has just you in it, two-factor is optional. ## Set up two-factor You need an authenticator app on your phone or computer, such as any app that supports time-based one-time codes. 1. Open **Settings** and, under **Security**, select **Set up two-factor**. 2. Enter your **Current password** and select **Begin setup**. 3. Scan the QR code with your authenticator app. If you can't scan it, use **Copy key** to enter the key by hand, or **Open in authenticator app** on a device that has one. 4. Enter the **Six-digit code** your app shows and select **Turn on two-factor**. Nothing changes until you enter a code. Finish within 15 minutes, in the same browser; otherwise you'll see **Start the setup again — that code has expired.** Select **Start again with a new code**, enter your password and select **Start again**. ![Two-factor setup with the QR code](/images/docs/screens/two-factor-setup-light.webp) > [!TIP] > Signed up with GitHub and never set a password? Setup asks for one. Select **Email me a reset link and sign me out**, set a password from the emailed link, then sign in and finish setup. ### Save your recovery codes As soon as two-factor is on, Folioso shows you 8 recovery codes. **This is the only time they're shown.** - Use **Copy codes** or **Download** to keep them somewhere safe, such as a password manager. - Each code works once, in place of an authenticator code. - Select **I have saved them** when you're done. > [!WARNING] > If you lose your authenticator app and your recovery codes, you can't complete the code prompt when you sign in with your password. Keep your recovery codes safe. ### What changes when you turn it on Any device you'd asked Folioso to remember is forgotten. The next time you sign in on it, you'll need your password and a code. The device you used to set up stays signed in for now. ## Sign in with two-factor After your email and password, enter the code from your app. If you don't have your device, select **Lost your device? Use a recovery code**. See [Signing in](/account/signing-in#the-two-factor-code-prompt). Two-factor applies to password sign-in. Signing in with GitHub or your organisation's single sign-on doesn't ask for a code, because your identity there is already the second factor. ## Manage your recovery codes **Settings** shows how many unused recovery codes you have left. To get a fresh set: 1. Under **Security**, select **Manage two-factor**. 2. Enter your **Current password** and select **Regenerate recovery codes**. Your old codes stop working, and the new ones are shown once. ## Turn off two-factor If two-factor isn't required for you: 1. Under **Security**, select **Manage two-factor**. 2. Enter your **Current password** and select **Turn off**. If you're an admin or owner of an organisation with other members, **Turn off** isn't offered. ## Move to a new authenticator Folioso replaces an authenticator by turning two-factor off and setting it up again. You can't start a new setup while two-factor is on. - **If two-factor is optional for you**, sign in (with a recovery code if you need one), select **Manage two-factor**, **Turn off**, then **Set up two-factor** on the new device. - **If it's required**, you can't turn it off. Move the existing entry to your new device with your authenticator app's own transfer or backup feature, and keep using your recovery codes until you have. If that isn't possible, the requirement lifts while you aren't an admin or owner of an organisation with other members, so if you aren't the owner, another admin can change your role to member while you set it up again, then make you an admin again. --- # API tokens Create personal API tokens to use the Folioso REST API from scripts and integrations. Each token acts as you, in one organisation, with read-only or read-and-write access. ![The API tokens section of account settings](/images/docs/screens/settings-api-tokens-light.webp) ## Who can create tokens - The REST API is available if your organisation's plan includes API access. If it doesn't, the section says **The REST API is available on the Pro plan and above.** See the [pricing page](https://folioso.dev/pricing). - Any member of the organisation can create tokens. You don't need to be an admin. - A token can never do more than you can. If you're a viewer in a space, your token can read it but not change it. If you lose access to a space or page, so does your token. ## One organisation per token A token is bound to the organisation you created it in, and works only at that organisation's address. A leaked token reaches nothing else. If you belong to several organisations, create a token in each one you want to automate. The **API tokens** section shows the organisation's name and the exact base address to send requests to, for example `https://acme.folioso.dev/api/v1`. It lists only the tokens you created in this organisation. ## Create a token 1. Open **Settings** while you're in the organisation, and go to **API tokens**. 2. Enter a **Token name** (up to 60 characters) so you can recognise it later, for example *Deploy script*. 3. Under **Access**, choose **Read only** or **Read and write**. 4. Under **Expires**, choose **Never**, **In 30 days**, **In 90 days** or **In a year**. 5. Select **Create token**. 6. Copy the token from the panel that appears and store it somewhere safe. > [!IMPORTANT] > The token is shown **only once**. Folioso keeps only a fingerprint of it and can't show it again. If you lose it, revoke it and create a new one. ## Access levels | Access | Can do | | --- | --- | | **Read only** | List spaces, groups, tags and pages, search, and read pages. | | **Read and write** | Everything a read-only token can, plus create, update and delete pages (deleting moves a page to the recycle bin). | See the [API overview](/api/overview) for the full list of endpoints. ## Use a token Send the token in the `Authorization` header of every request: ```bash curl https://acme.folioso.dev/api/v1/me \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" ``` See [Authentication and limits](/api/authentication). ## Track and revoke tokens Each token in the list shows its access (**Read/write** or **Read only**), when it was last used (or **Never used**), when it was created and, if it has one, when it expires. Expired tokens are marked **Expired** and stop working. To revoke a token, select **Revoke** next to it. It stops working immediately. You can always revoke tokens, even if your organisation has moved to a plan without API access. ## When a token stops working A token is refused when: - it has expired or been revoked; - your organisation's plan no longer includes API access; - you're no longer a member of the organisation, or your membership is suspended (your tokens work again if the suspension is lifted); - your account must use [two-factor authentication](/account/two-factor-authentication) and you haven't set it up yet. --- # Exporting your data Download a copy of everything you've written in Folioso: every page you created, as Markdown, plus every image you uploaded, bundled into one ZIP file. ![The data export section of account settings](/images/docs/screens/settings-export-light.webp) ## Request an export 1. Open **Settings** and scroll to **Data**. 2. Select **Export all data**. 3. You'll see **Export started — we'll email you a download link when it's ready.** Folioso builds the archive in the background. When it's ready, you get an email with a download link. You don't need to keep the page open. Selecting the button again while an export is being prepared doesn't start a second one. You can request up to 6 exports an hour. ## Download it Open the link in the email. The link: - is valid for **24 hours**; - only works while you're signed in to your own account, so forwarding it doesn't give anyone else access. After the link expires the archive is deleted. Request a new export if you need another copy. Each new export replaces the previous one. ## What's in the archive | Folder | Contents | | --- | --- | | `README.md` | A summary of what's in the archive. | | `pages/` | Every page you created that isn't deleted, as a Markdown file, in a folder for its space and, if it has one, its group. | | `recycle-bin/` | Pages you created that are in a recycle bin. | | `images/` | Every image you uploaded, as WebP files. | Images inside your pages point at the copies in `images/`, so the archive works offline. Links to your own pages point at the matching Markdown file; links to pages you don't own point at the page in Folioso. The export includes pages you created in **every** organisation you belong to. ## What's not included - Pages other people created, even if you've edited them. - Version history snapshots, and your account settings. To save a single page instead, see [Exporting pages](/editor/exporting). --- # Deleting your account Deleting your account signs you out everywhere and schedules your account for permanent erasure after a 30-day grace window. Until then you can change your mind and get everything back. ![The delete account section of account settings](/images/docs/screens/settings-delete-account-light.webp) ## Before you start - **Export anything you want to keep.** After the grace window nothing can be recovered. See [Exporting your data](/account/exporting-your-data). - **Hand over your organisations.** You can't delete your account while you're the only admin of an organisation that has other members. Make someone else an admin first; otherwise you'll see **Promote another admin first.** See [Admins and ownership](/organisation/roles-and-ownership). - **You need a password.** If you signed up with GitHub and never set one, set a password first. See [Profile and email](/account/profile#set-a-password-for-a-github-account). ## Delete your account 1. Open **Settings** and scroll to **Danger Zone**. 2. Select **Delete account…**. 3. Enter your password under **Confirm your password**. 4. Select **Delete my account**. What happens straight away: - You're signed out on every device, and you can't sign in to the app. - You're taken to the sign-in page, which shows the date your account will be erased. - Folioso emails you the same date. ## Change your mind You have 30 days. To cancel the deletion: 1. Sign in as usual, with your password or with GitHub. 2. Instead of opening the app, Folioso emails you a reactivation link. 3. Open the link within 60 minutes and select **Reactivate my account**. Your account comes back exactly as you left it: pages, images, shares and settings. If the link has expired, sign in again to get a new one. If you want the deletion to go ahead, do nothing. ## What's erased at the end of the grace window When the 30 days are up, Folioso permanently erases your identity. What happens to your content depends on where it is. | Where | What happens | | --- | --- | | Organisations where you're the **only member** | The whole organisation is erased, with all its spaces, pages and images. | | Organisations with **other members** | You're removed from the organisation. The organisation and its content carry on. | | Your personal space ("My space") in every organisation | Erased, with all its pages, including pages in its recycle bin. | | Pages you created in shared spaces | Kept. They show their author as **Former member**. | | Your private pages in shared spaces | Kept, and shown to the organisation's admins under former members' content, where they can transfer or delete them. | | Images in pages that are kept | Kept, so those pages still display them. | | Your profile photo, notifications, sessions and API tokens | Erased. | | Pages shared with you | Your access is removed. | | Public links and shares you created on pages that are kept | Stay active. | See [Former members' content](/organisation/former-members-content) for what admins can do with pages left behind. > [!NOTE] > Your name can still appear where other people mentioned you inside their pages, and in past notification history. Removing it would mean changing other people's content. ## Leaving one organisation instead If you only want to stop being part of one organisation, you don't need to delete your account. See [Working in several organisations](/organisation/multiple-organisations). --- # Security overview How Folioso protects your account, your organisation's content and the pages you share: sign-in protection, two-factor authentication, sessions, public links, uploads and the audit log. ## Signing in - **Passwords** need at least 8 characters, with upper- and lower-case letters, a number and a symbol. - **Repeated failed sign-ins** are slowed down: after several wrong attempts for an address in a minute, sign-in for it is paused briefly. - **The sign-in form doesn't reveal whether an account exists.** A wrong password and an unknown address get the same answer, in the same time. - **Password reset links** expire after 60 minutes and don't sign you in by themselves. - **GitHub and single sign-on** link to an existing account only when the email address is verified on both sides, so nobody can claim your account by registering your address first. - **Single sign-on never creates accounts.** It only signs in people an admin has already added, and it checks membership again on every sign-in, so someone who's been removed can't keep signing in through your identity provider. See [Signing in](/account/signing-in). ## Two-factor authentication Anyone can turn on two-factor authentication with an authenticator app. It's **required** for admins and owners of an organisation with more than one member: until they set it up, the only page they can open is the setup page, their API tokens are refused, and they can't switch it off. - Codes can't be reused: a code that's just been accepted is refused if it's tried again. - Recovery codes work once each and are shown only when they're created. - Turning two-factor on forgets every device you'd asked Folioso to remember, so each needs a code at its next sign-in. See [Two-factor authentication](/account/two-factor-authentication). ## Sessions - **Changing your password signs out every other device** and forgets remembered devices. The device you changed it on stays signed in. - **Deleting your account signs you out everywhere** at once. - **Changing your email address** emails the previous address, so a change you didn't make doesn't go unnoticed. If your account has a password, changing your email address or password asks for it first. - Changes to your email address and password are recorded in the audit log of every organisation you belong to. ## Who can see what Spaces are the permission boundary. Each person's access to a space comes from their space role (viewer, editor or admin), their teams, or a page shared with them directly. Private pages are visible only to their author and the people they're shared with. Each member's personal space ("My space") is visible only to them, and not to organisation admins. Folioso doesn't confirm whether a page or organisation exists to people who can't see it: they get the same "not found" answer as for something that doesn't exist. See [Permissions](/spaces/permissions). ## Public links - **Kept out of search engines.** Every public link page tells search engines not to index, follow, archive or snippet it, and search engines are asked not to crawl public links at all. Link previews in chat apps still work, so a shared link still unfurls. - **Optional passwords.** You can protect a public link with a password (6 to 72 characters). The page isn't shown until the password is entered, and the password prompt shows nothing from the page. Wrong guesses are rate-limited. - **Changing or removing the password** ends access for everyone who unlocked the link with the old one. - **Read-only or edit.** You choose whether a link lets people edit. See [Public links](/sharing/public-links). ## Uploaded images Every image you upload is checked by its actual contents, not just its file name, and then re-encoded by Folioso into a new WebP file. The original file is never served as it was uploaded. Uploaded images get random names, so an image's address can't be guessed from another. See [Images](/editor/images). ## API tokens - Each token is bound to **one** organisation and only works at that organisation's address. - A token can be read-only or read-and-write, and can never do more than the person who created it. - Folioso stores only a fingerprint of each token. The token itself is shown once, when you create it. - Tokens can be given an expiry date and revoked at any time. See [API tokens](/account/api-tokens). ## Audit log Organisations record security-relevant events in an audit log: invitations, membership and role changes, ownership transfers, email and password changes, suspensions, space access changes, sharing and public links, page visibility changes, organisation settings, single sign-on and domain changes. Each entry records who did it and when. Recording is always on, on every plan. Viewing and exporting the log needs a plan that includes the audit log. Entries are kept for 365 days. See [Audit log](/organisation/audit-log). ## Your data For how long things are kept, how to export your data and what deleting your account erases, see [Data and privacy](/security/data-and-privacy). --- # Data and privacy What Folioso keeps, for how long, and how you can take your data with you or have it erased. ## Where your content lives Your pages, images and settings belong to the organisation they were created in. Each organisation is separate: its content is visible only to people with access in that organisation and to people a page has been shared with, and an API token works in one organisation only. Search runs inside Folioso. Your pages aren't sent to an outside search service, and [public links](/sharing/public-links) are kept out of search engines. ## How long things are kept | What | How long | | --- | --- | | **Pages in a recycle bin** | 30 days, then permanently deleted. You can restore a page before then. See [Recycle bin](/spaces/recycle-bin). | | **Automatic version history snapshots** | Set by your organisation's plan, and kept indefinitely on some plans. See the [pricing page](https://folioso.dev/pricing). A page's most recent snapshot is always kept. | | **Manual snapshots** and the snapshot saved before a restore | Kept until the page is permanently deleted. See [Version history](/editor/version-history). | | **Space activity feed** | 90 days. | | **Notifications** | 90 days, read or not. | | **Organisation audit log** | 365 days. | | **PDF exports of a page** | 7 days after they're created. | | **Your full data export** | Until its download link expires, 24 hours after it's emailed. | | **An account scheduled for deletion** | 30 days, then erased. | ## Export your data You can download everything you've created at any time: - **Your whole account**: every page you created, in every organisation, as Markdown, plus every image you uploaded, in one ZIP file. See [Exporting your data](/account/exporting-your-data). - **A single page**: as Markdown, or as PDF if your organisation's plan includes PDF export. See [Exporting pages](/editor/exporting). ## Delete your account Deleting your account signs you out everywhere straight away and erases your account after a 30-day grace window, during which you can reactivate it. When it's erased: - organisations where you were the only member are erased with all their content; - you're removed from organisations that have other members, and your personal space in each is erased; - pages you created in shared spaces stay with the organisation and show **Former member** as their author; - your profile photo, sessions, notifications and API tokens are erased, and so is any data export waiting for you. Your name can remain where other people mentioned you inside their pages. See [Deleting your account](/account/deleting-your-account). ## Delete an organisation An organisation can be deleted with everything in it. See [Deleting an organisation](/organisation/deleting-an-organisation). ## When someone leaves an organisation Removing a member doesn't delete their pages. Pages in shared spaces stay where they are. Private pages and personal spaces left behind by people who are no longer members are listed for admins to transfer or delete. See [Former members' content](/organisation/former-members-content). ## Audit log privacy The audit log never names content the reader couldn't open: an event about a page in someone's personal space isn't recorded, and an event about a private page is recorded without its title. See [Audit log](/organisation/audit-log). --- # API overview The Folioso REST API lets scripts and integrations read your organisation's spaces, groups, tags and pages, search them, and create, update, move, tag and delete pages, with their content as TipTap JSON or Markdown. It can also upload images, read your recycle bin, and read and take page snapshots. It's available if your organisation's plan includes API access. See the [pricing page](https://folioso.dev/pricing). ## What you can do | Method | Endpoint | Needs | What it does | | --- | --- | --- | --- | | `GET` | `/me` | read | Who the token belongs to, its organisation and its abilities. | | `GET` | `/spaces` | read | The spaces you can reach. | | `GET` | `/groups` | read | Groups in those spaces. | | `GET` | `/tags` | read | Tags in those spaces. | | `GET` | `/pages` | read | List pages, or search them with `q`. | | `GET` | `/pages/{slug}` | read | One page, with its content. | | `POST` | `/pages` | write | Create a page, with its group and tags. | | `PATCH` | `/pages/{slug}` | write | Rename a page, replace its content, move it, or set its tags. | | `DELETE` | `/pages/{slug}` | write | Move a page to the recycle bin. | | `POST` | `/images` | write | Upload an image to use on a page. | | `GET` | `/recycle-bin` | read | The pages in your recycle bin. | | `GET` | `/recycle-bin/{slug}` | read | One page in your recycle bin, with its content. | | `GET` | `/pages/{slug}/snapshots` | read | A page's snapshots. | | `GET` | `/pages/{slug}/snapshots/{id}` | read | One snapshot, with its content. | | `POST` | `/pages/{slug}/snapshots` | write | Take a snapshot of a page. | The API covers pages, their images and history, and reading their surroundings. Tags are created when you name them on a page. Groups, spaces, sharing and members are managed in the app, and so are restoring pages and snapshots. ## Base address The API lives on **your organisation's address**, under `/api/v1`: ```http https://{slug}.folioso.dev/api/v1 ``` For an organisation at `acme.folioso.dev`, that's `https://acme.folioso.dev/api/v1`. The exact address is shown in **Settings** under **API tokens**. If your organisation changes its address, requests to the old one are redirected for 30 days. See [Errors](/api/errors#308-organisation-has-moved). ## Your first request 1. [Create a token](/account/api-tokens) in **Settings**. 2. Call `/me` to check it works: ```bash curl https://acme.folioso.dev/api/v1/me \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" ``` ```json { "data": { "user": { "id": 5, "name": "Ada Lovelace", "email": "ada@example.com" }, "organisation": { "slug": "acme", "name": "Acme" }, "token": { "name": "Deploy script", "abilities": ["read", "write"], "lastUsedAt": "2026-10-16T09:12:44+00:00", "expiresAt": null } } } ``` ## How the API behaves - **It acts as you.** A token has exactly your access in its organisation, and never more. Lists only include what you can see, and writes follow the same permissions as the app. See [Permissions](/spaces/permissions). - **Changes are the same as in the app.** Pages created, edited or deleted through the API show in the space's activity feed and notify people you mention, just as they would from the editor. - **JSON in, JSON out.** Every response, including errors, is JSON. Send request bodies as JSON with `Content-Type: application/json`. The one exception is an image upload, which is sent as `multipart/form-data`. - **Pages are identified by their slug**, a UUID such as `9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b`. It's the last part of the page's address in the app. Page objects also carry a numeric `id`, which is what a link from one page's content to another names. - **Your user id** is `data.user.id` in the `/me` response above. It's what a [mention](/api/markdown-extensions#mentions) of you names. The API doesn't list other members, so another person's id can only come from a mention already in content you can read. - **Spaces are identified by their slug**, groups and tags by a numeric `id`. - **Dates** are ISO 8601 with a timezone offset. - **Field names** in responses are camelCase (`createdAt`); request fields use snake_case where they have two words (`group_id`, `per_page`). ## Page content A page's body is stored as a JSON document in the TipTap format, the format the editor uses. The API returns it as `content` and accepts the same shape when you create or update a page. You can also read and write it as Markdown: add `?format=markdown` to get a `markdown` string instead, and send `markdown` instead of `content`. Markdown you read and send back unchanged keeps everything on the page. HTML isn't accepted. See [Content formats](/api/content-formats). ## Reference - [Authentication and limits](/api/authentication): tokens, abilities and rate limits. - [Pages](/api/pages): list, search, read, create, update, move, tag and delete pages. - [Content formats](/api/content-formats): TipTap JSON or Markdown, and the round trip. - [Writing Markdown](/api/markdown) and [Folioso Markdown extensions](/api/markdown-extensions). - [TipTap JSON reference](/api/tiptap-json): every node, mark and attribute. - [Images](/api/images): upload images for your pages. - [Spaces, groups and tags](/api/spaces-groups-and-tags). - [Recycle bin and snapshots](/api/recycle-bin-and-snapshots). - [Examples](/api/examples): complete workflows. - [Errors](/api/errors): status codes and error bodies. --- # Authentication and limits Every API request is authenticated with a personal API token sent as a bearer token. Tokens are bound to one organisation, carry read or read-and-write abilities, and are rate-limited per token and per IP address. ## Create a token Tokens are created in the app, not through the API: 1. Open **Settings** in the organisation you want to automate. 2. Under **API tokens**, enter a **Token name**, choose **Access** (**Read only** or **Read and write**) and **Expires**. 3. Select **Create token** and copy it straight away. It's shown only once. See [API tokens](/account/api-tokens) for the details. The API needs a plan that includes API access. See the [pricing page](https://folioso.dev/pricing). ## Send the token Put the token in the `Authorization` header, with the `Bearer` scheme: ```http GET /api/v1/spaces HTTP/1.1 Host: acme.folioso.dev Authorization: Bearer YOUR_TOKEN Accept: application/json ``` ```bash curl https://acme.folioso.dev/api/v1/spaces \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" ``` A missing, wrong, revoked or expired token gets `401 Unauthenticated`. ## Use your organisation's address A token works only at the address of the organisation it was created in. Sending it to another organisation's address gets `404`, the same answer as for an organisation that doesn't exist, so a token can't be used to find out which organisations exist. If your organisation's address changes, see [Errors](/api/errors#308-organisation-has-moved). ## Abilities Each token holds one or both of two abilities: | Token access | Abilities | Allowed endpoints | | --- | --- | --- | | **Read only** | `read` | All `GET` endpoints. | | **Read and write** | `read`, `write` | All `GET` endpoints, plus `POST /pages`, `PATCH /pages/{slug}`, `DELETE /pages/{slug}`, `POST /images` and `POST /pages/{slug}/snapshots`. | A read-only token calling a write endpoint gets `403` with `"message": "Invalid ability provided."`. Call `GET /me` to see a token's abilities. Abilities limit what kind of request a token can make. What it can reach is decided by your own permissions: a read-and-write token still can't edit a page you couldn't edit in the app. ## When a valid token is refused | Status | Message | Why | | --- | --- | --- | | `403` | `Your plan does not include this feature.` | The organisation's plan doesn't include API access. | | `403` | `You are no longer a member of this organisation.` | You've left or been removed from the organisation. | | `403` | `Your account in this organisation is suspended. Contact an administrator.` | Your membership is suspended. The token works again if the suspension is lifted. | | `403` | `This account must enrol in two-factor authentication before its API tokens can be used.` | You're an admin or owner of an organisation with other members and haven't set up [two-factor authentication](/account/two-factor-authentication). | ## Rate limits Two limits apply to every request: | Limit | Applies to | Allowance | | --- | --- | --- | | Per token | Each token separately | 120 requests a minute | | Per IP address | Every request to the API from one address, signed in or not | 300 requests a minute | Each token has its own allowance, so two integrations with different tokens don't share one. Two kinds of request have a further allowance of their own. These belong to **your account**, not the token, and are shared with the same actions in the app: | Request | Allowance | | --- | --- | | `POST /images` | 50 images an hour, shared with every image you upload in the app: images in the editor, avatars and logos. | | `POST /pages/{slug}/snapshots` | 10 a minute, shared with taking and restoring snapshots in the app. | Responses within the per-token limit carry headers showing your allowance: ```http X-RateLimit-Limit: 120 X-RateLimit-Remaining: 117 ``` Going over either limit gets `429 Too Many Requests` with a `Retry-After` header giving the number of seconds to wait: ```json { "message": "Too Many Attempts." } ``` The per-IP limit answers `"message": "Too many requests. Slow down and try again shortly."`. > [!TIP] > When you get a `429`, wait for the number of seconds in `Retry-After` before retrying, rather than retrying immediately. ## Keeping tokens safe - Give each integration its own token, so you can revoke one without breaking the others. - Use **Read only** unless the integration needs to write. - Set an expiry where you can, and revoke tokens you no longer use. - Never commit a token to a repository. Store it in your platform's secret store or an environment variable. --- # Pages List, search, read, create, update, move, tag and delete pages through the API. Pages are identified by their slug, and their content can be read and written as TipTap JSON or as Markdown. All examples use `https://acme.folioso.dev/api/v1`; use your organisation's address instead. ## The page object ```json { "id": 42, "slug": "9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b", "title": "Deploying the API", "excerpt": "How we ship the API service to production, step by step.", "visibility": "space", "space": { "slug": "engineering", "name": "Engineering" }, "group": { "id": 12, "name": "Runbooks" }, "tags": [ { "id": 31, "name": "php" } ], "content": { "type": "doc", "content": [ { "type": "paragraph", "content": [{ "type": "text", "text": "How we ship the API service to production, step by step." }] } ] }, "contentEpoch": 0, "url": "https://acme.folioso.dev/engineering/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b", "createdAt": "2026-10-01T14:03:11+00:00", "updatedAt": "2026-10-16T09:12:44+00:00" } ``` | Field | Type | Description | | --- | --- | --- | | `id` | integer | The page's number. Every endpoint takes the slug, not this; use the `id` to link to the page from another page's content, as a [`folioso:page`](/api/markdown-extensions#page-links) link or a [`pageLink`](/api/tiptap-json#pagelink) node. | | `slug` | string | The page's identifier, a UUID. Use it in `/pages/{slug}`. | | `title` | string | The title. Can be empty for an untitled page. | | `excerpt` | string or null | A short plain-text preview of the content. `null` until the page has been indexed for search since it last changed, which can take up to about 40 minutes. A rename or a move counts as a change, as well as an edit. Always `null` on [recycle bin](/api/recycle-bin-and-snapshots) items. | | `visibility` | string | `space` (visible to the space's members) or `private` (visible to the author and the people it's shared with). | | `space` | object | The space's `slug` and `name`. | | `group` | object or null | The group's `id` and `name`, or `null` if the page isn't in a group. | | `tags` | array | The page's tags, each with `id` and `name`. Only returned for a single page. | | `content` | object or null | The page body as a TipTap JSON document. Only returned for a single page. | | `markdown` | string or null | The page body as Markdown, in place of `content`, when you ask for `?format=markdown`. See [Content formats](/api/content-formats). | | `contentEpoch` | integer | Changes when the page is restored from a snapshot or its body is changed through the API. Returned with `content` or `markdown`. See [Avoid overwriting a restore](#avoid-overwriting-a-restore). | | `url` | string | The page's address in the app. | | `createdAt`, `updatedAt` | string | ISO 8601 timestamps. | Lists leave out `tags`, the body and `contentEpoch`, to keep them small. Fetch a single page to get them. ## Page content A page's body is stored as a [TipTap](https://tiptap.dev) JSON document: a `doc` node whose `content` is an array of block nodes, each of which can hold text nodes with marks. ```json { "type": "doc", "content": [ { "type": "heading", "attrs": { "level": 2 }, "content": [{ "type": "text", "text": "Install" }] }, { "type": "paragraph", "content": [ { "type": "text", "text": "Run " }, { "type": "text", "text": "composer install", "marks": [{ "type": "code" }] }, { "type": "text", "text": " first." } ] }, { "type": "codeBlock", "attrs": { "language": "bash" }, "content": [{ "type": "text", "text": "composer install --no-dev" }] } ] } ``` You can read and write the same body as Markdown instead: ````markdown ## Install Run `composer install` first. ```bash composer install --no-dev ``` ```` - **Reading:** single-page responses return `content` by default, or `markdown` with `?format=markdown`. - **Writing:** send the body as `content`, a TipTap JSON object, or as `markdown`, a string, but not both. `null` for either empties the page. - **Round trip:** Markdown you read and send back unchanged stores the same document, with nothing lost. - Mentions in the content notify the people mentioned, and page links update the target's **Linked from**, as they do in the editor. See [Content formats](/api/content-formats) for the rules, [Writing Markdown](/api/markdown) and [Folioso Markdown extensions](/api/markdown-extensions) for Markdown, and the [TipTap JSON reference](/api/tiptap-json) for every node and mark. To put images on a page, see [Images](/api/images). ## List pages ```http GET /pages ``` Lists the pages you can see in the organisation, most recently updated first. Pages in a recycle bin aren't included. | Parameter | Description | | --- | --- | | `space` | Only pages in the space with this slug. | | `group` | Only pages in the group with this `id`. | | `tag` | Only pages with the tag with this `id`. | | `q` | Search instead of listing. See [Search pages](#search-pages). | | `per_page` | Results per page, from 1 to 100. Default 25. A value outside that range is treated as the nearest limit, not refused. | | `page` | Which page of results to return. Default 1. | An unknown `space`, `group` or `tag` gives an empty list, not an error. ```bash curl "https://acme.folioso.dev/api/v1/pages?space=engineering&per_page=2" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" ``` ```json { "data": [ { "id": 42, "slug": "9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b", "title": "Deploying the API", "excerpt": "How we ship the API service to production, step by step.", "visibility": "space", "space": { "slug": "engineering", "name": "Engineering" }, "group": { "id": 12, "name": "Runbooks" }, "url": "https://acme.folioso.dev/engineering/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b", "createdAt": "2026-10-01T14:03:11+00:00", "updatedAt": "2026-10-16T09:12:44+00:00" }, { "id": 1207, "slug": "1f0e2d3c-5b6a-4978-8a9b-0c1d2e3f4a5b", "title": "On-call checklist", "excerpt": null, "visibility": "private", "space": { "slug": "engineering", "name": "Engineering" }, "group": null, "url": "https://acme.folioso.dev/engineering/1f0e2d3c-5b6a-4978-8a9b-0c1d2e3f4a5b", "createdAt": "2026-10-15T08:00:02+00:00", "updatedAt": "2026-10-15T08:41:19+00:00" } ], "links": { "first": "https://acme.folioso.dev/api/v1/pages?space=engineering&per_page=2&page=1", "last": "https://acme.folioso.dev/api/v1/pages?space=engineering&per_page=2&page=4", "prev": null, "next": "https://acme.folioso.dev/api/v1/pages?space=engineering&per_page=2&page=2" }, "meta": { "current_page": 1, "from": 1, "last_page": 4, "path": "https://acme.folioso.dev/api/v1/pages", "per_page": 2, "to": 2, "total": 8 } } ``` ### Pagination List responses are paginated. `meta.total` is the number of matching pages and `meta.last_page` the number of result pages. Follow `links.next` until it's `null` to read them all; it keeps your other parameters. `meta` also carries a `links` array of numbered page links, for building page controls. ## Search pages ```http GET /pages?q={terms} ``` Add `q` to search page titles and content instead of listing. The same search powers the search box in the app. See [Search](/sharing/search). - `q` can be up to 200 characters. - Pages whose **title** matches come first, then the rest by relevance, then by most recently updated. - You can combine `q` with `space`, `group`, `tag`, `per_page` and `page`. - Search results include pages shared with you directly, even in spaces you can't otherwise open. The plain list doesn't include those. - Titles are searchable straight away. New or changed content becomes searchable about 30 to 40 minutes after the last edit. ```bash curl -G "https://acme.folioso.dev/api/v1/pages" \ --data-urlencode "q=deploy checklist" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" ``` The response has the same shape as [List pages](#list-pages). A blank `q` returns the normal list. A `q` with no letters or digits in it, such as `--` or `!?`, has nothing to search for and returns an empty list. ## Get a page ```http GET /pages/{slug} ``` Returns one page, with its `tags`, `content` and `contentEpoch`. Needs the `read` ability and permission to view the page. | Parameter | Description | | --- | --- | | `format` | `json` (the default) returns the body as `content`. `markdown` returns it as `markdown`. Anything else is a `422`, and so is `markdown` for the rare page whose stored content can't be written as Markdown without loss. See [When Markdown isn't available](/api/content-formats#when-markdown-isnt-available). | ```bash curl https://acme.folioso.dev/api/v1/pages/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" ``` ```json { "data": { "id": 42, "slug": "9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b", "title": "Deploying the API", "excerpt": "How we ship the API service to production, step by step.", "visibility": "space", "space": { "slug": "engineering", "name": "Engineering" }, "group": { "id": 12, "name": "Runbooks" }, "tags": [{ "id": 31, "name": "php" }], "content": { "type": "doc", "content": [] }, "contentEpoch": 0, "url": "https://acme.folioso.dev/engineering/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b", "createdAt": "2026-10-01T14:03:11+00:00", "updatedAt": "2026-10-16T09:12:44+00:00" } } ``` A page in another organisation, a page in a recycle bin and a slug that doesn't exist all get `404`. A page in this organisation that you aren't allowed to view gets `403`. ## Create a page ```http POST /pages ``` Creates a page in a space. Needs the `write` ability and the editor or admin role in the space (or an organisation admin). | Field | Rules | Description | | --- | --- | --- | | `title` | Required. Text, up to 255 characters. | The title. Leading and trailing spaces are trimmed. | | `space` | Required. A space slug. | The space to create the page in. | | `group_id` | Optional. An integer. | A group in that space. Leave it out (or send `null`) to create the page outside any group. | | `content` | Optional. A JSON object or `null`. | The body as a TipTap document. Leave it out for an empty page. | | `markdown` | Optional. Text or `null`. | The body as Markdown, instead of `content`. | | `tags` | Optional. An array of up to 50 tag names. | The page's tags, by name. See [Tags](#tags). | | `format` | Optional. `json` or `markdown`. | The format of the body in the response. Usually sent in the query string. | New pages are created with `space` visibility, so everyone in the space can see them, and are placed at the end of their group, not nested under another page. You'll be the author. The API can't make a page private or nest it under another page; do that in the app. ```bash curl https://acme.folioso.dev/api/v1/pages \ -X POST \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "title": "Release notes 4.2", "space": "engineering", "group_id": 12, "content": { "type": "doc", "content": [ { "type": "paragraph", "content": [{ "type": "text", "text": "What changed in 4.2." }] } ] } }' ``` The same page, with its body in Markdown and two tags: ```bash curl "https://acme.folioso.dev/api/v1/pages?format=markdown" \ -X POST \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "title": "Release notes 4.2", "space": "engineering", "group_id": 12, "tags": ["release", "php"], "markdown": "What changed in 4.2." }' ``` The response is `201 Created` with the new page, including `tags`, `content` (or `markdown`) and `contentEpoch`, in the same shape as [Get a page](#get-a-page). If you asked for `?format=markdown` and the content you sent can't be written back as Markdown without loss, the page is still created and the response carries `content` instead of `markdown`. See [When Markdown isn't available](/api/content-formats#when-markdown-isnt-available). The request is checked in this order, and refused at the first problem. Nothing is created when a request is refused. | Status | When | | --- | --- | | `422` | A field is missing or invalid, or `content` and `markdown` were both sent. | | `404` | `No such space in this organisation.` Also the answer for a space you can't see, such as a private space you have no role in, so the response never confirms that one exists. | | `403` | You can't add pages to that space. | | `404` | `No such group in that space.` A group must belong to the space you name. | | `422` | The Markdown has a malformed [Folioso extension](/api/markdown-extensions#error-messages). | ## Update a page ```http PATCH /pages/{slug} ``` Renames a page, replaces its content, moves it to another group or space, or sets its tags, in any combination. It can't change the page's visibility or nest it under another page. Needs the `write` ability and permission to edit the page: an editor or admin in its space, the page's author, or someone it's shared with for editing. Moving and tagging need more. See [Move, regroup and tag a page](#move-regroup-and-tag-a-page). Send only the fields you want to change: | Field | Rules | Description | | --- | --- | --- | | `title` | Optional. Text up to 255 characters, or `null`. | The new title. An empty title or `null` leaves the page untitled. | | `content` | Optional. A JSON object or `null`. | Replaces the whole body. `null` empties the page. Leave the field out to keep the current content. | | `markdown` | Optional. Text or `null`. | Replaces the whole body with Markdown, instead of `content`. `null` or `""` empties the page. | | `epoch` | Optional. An integer, 0 or more. | The `contentEpoch` you last read. See below. | | `space` | Optional. A space slug. | Moves the page to this space. | | `group_id` | Optional. An integer, or `null`. | Moves the page into this group, or out of any group with `null`. | | `tags` | Optional. An array of up to 50 tag names. | Replaces the page's tags. See [Tags](#tags). | | `format` | Optional. `json` or `markdown`. | The format of the body in the response. Usually sent in the query string. | `content` and `markdown` replace the body in full; there's no partial update. Read the page, change the document or the Markdown, and send it back. See [Content formats](/api/content-formats). ```bash curl https://acme.folioso.dev/api/v1/pages/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b \ -X PATCH \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "title": "Deploying the API (v2)" }' ``` The response is `200 OK` with the updated page, in the same shape as [Get a page](#get-a-page). As on create, a `?format=markdown` response falls back to `content` when the page's content can't be written as Markdown without loss; the update has still been made. Changing the body, through `content` or `markdown`, gives the page a new `contentEpoch`, which the response returns. Anyone with the page open in the editor has it reloaded with your content. Sending a body identical to the stored one changes nothing: the epoch stays the same and nobody is reloaded. > [!WARNING] > Leaving `content` and `markdown` out keeps the body as it is, but sending `"content": null`, `"markdown": null` or `"markdown": ""` empties the page. ### Avoid overwriting a restore If someone restores a page from a [version history](/editor/version-history) snapshot while your integration is working on it, writing back the copy you read earlier would undo the restore. To prevent that, send the `contentEpoch` you read as `epoch`: ```json { "markdown": "## Install\n\nRun `composer install` first.", "epoch": 3 } ``` If the page has been restored, or its content replaced through the API, since you read it, the update is refused with `409 Conflict` and nothing changes. Read the page again and reapply your change. If you don't send `epoch`, no check is made. Your own content updates change the epoch too whenever they change the body, so always send the `contentEpoch` from your last response, not the one you first read. A content update reloads anyone with the page open. Someone editing it on their own is reloaded on their next save, and anything they typed since their last save is lost. ### Move, regroup and tag a page `space`, `group_id` and `tags` organise a page. Sending any of them needs more than permission to edit: the editor or admin role in the page's space, or authorship of the page. Someone the page is shared with for editing can change its title and body but not these, and gets `403` if they send any of them. - **Within its space:** send `group_id` to move the page into another group of the same space, or `null` to take it out of its group. - **To another space:** send `space` with the target's slug. You need the editor or admin role in the target space. The page lands outside any group unless you also send `group_id` for a group in the target space. As with **Move to space…** in the app, the pages nested under it move too, its address doesn't change, and snapshots from before the move are hidden. See [Move a page](/editor/pages#to-another-space). - **Its own space:** sending the slug of the space the page is already in, without `group_id`, moves nothing. ```bash curl https://acme.folioso.dev/api/v1/pages/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b \ -X PATCH \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "space": "operations", "group_id": 27, "tags": ["runbook", "deploy"] }' ``` The request is checked in this order, and refused at the first problem. Everything before the `409` is checked before anything changes, so those refusals change nothing: | Status | When | | --- | --- | | `404` | The page doesn't exist or belongs to another organisation. | | `403` | You sent `space`, `group_id` or `tags` without the role to organise the page. | | `422` | A field is invalid. | | `403` | You can't edit the page. | | `404` | `No such space in this organisation.` Also the answer for a space you can't see. | | `403` | You can't add pages to the target space. | | `404` | `No such group in that space.` The group must be in the space the page ends up in. | | `403` | A tag must be created and you can't add tags to the space. | | `422` | The Markdown has a malformed [Folioso extension](/api/markdown-extensions#error-messages). | | `409` | The `epoch` you sent is out of date. | Once checked, the changes apply in order: the title and body first, then the move, then the tags. A `409` on the body stops the move and the tags too. A move to another space is recorded in the organisation's [audit log](/organisation/audit-log), as in the app. ### Tags `tags` on `POST /pages` and `PATCH /pages/{slug}` is the page's **complete** set of tags, by name: - Names match the space's existing tags exactly, including letter case. - A name the space doesn't have yet is created as a new tag there, with the editor's default colour. Creating a tag needs the editor or admin role in the space, as in the app. - On `PATCH`, the list replaces the page's tags, and `[]` removes them all. Leave `tags` out to keep them. - Names are text up to 50 characters each, with no blanks or repeats, and at most 50 in a list. `null` or a string instead of a list is a `422`. When a `PATCH` moves the page to another space, the names are matched in the target space. Without `tags`, the page's tags come with it, matched by name in the new space or created there, as in the app. See [Tags](/editor/tags). ## Delete a page ```http DELETE /pages/{slug} ``` Moves the page to the recycle bin, exactly like **Delete** in the app. Needs the `write` ability and permission to delete the page: its author, or an admin of its space or organisation. Editors who didn't write the page can't delete it. ```bash curl https://acme.folioso.dev/api/v1/pages/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b \ -X DELETE \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" ``` The response is `204 No Content`. The page can be restored from the [recycle bin](/spaces/recycle-bin) in the app until it's permanently deleted. The API can [read the recycle bin](/api/recycle-bin-and-snapshots#list-your-recycle-bin), but can't restore or permanently delete pages. --- # Content formats A page's body can be read and written in two formats: **TipTap JSON**, the document format the editor stores, and **Markdown**, in a dialect that reads like ordinary Markdown but carries everything the editor can store. This page covers choosing a format, the rules for empty bodies, how content writes affect people editing the page, and exactly what the Markdown round trip guarantees. ## The two formats | Format | Field | What it is | | --- | --- | --- | | TipTap JSON | `content` | The page's document exactly as stored: a `doc` node holding block nodes, text and marks. See [TipTap JSON reference](/api/tiptap-json). | | Markdown | `markdown` | A string of CommonMark with GitHub tables, task lists and strikethrough, plus a few Folioso extensions for what plain Markdown can't express. See [Writing Markdown](/api/markdown) and [Folioso Markdown extensions](/api/markdown-extensions). | The page is always stored as TipTap JSON. Markdown you send is converted to TipTap JSON on the way in, and Markdown you read is written from it on the way out. ## Reading Markdown Add `?format=markdown` to read a body as Markdown. `?format=json` is the default and can be left out. | Endpoint | Body returned | | --- | --- | | `GET /pages/{slug}` | The page's body. | | `POST /pages` and `PATCH /pages/{slug}` | The body of the page you created or updated. | | `GET /recycle-bin/{slug}` | The body of a page in your recycle bin. | | `GET /pages/{slug}/snapshots/{id}` | The body of a snapshot. | With `?format=markdown`, the response has a `markdown` string in place of `content`. Everything else stays the same, including `contentEpoch` on pages: ```bash curl "https://acme.folioso.dev/api/v1/pages/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b?format=markdown" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" ``` ```json { "data": { "slug": "9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b", "title": "Deploying the API", "markdown": "## Install\n\nRun `composer install` first.", "contentEpoch": 3 } } ``` The other fields are left out above. A page with no body at all answers `"markdown": null`. Any other `format` value is a `422` on `format`. A page you can't reach is still a `404`, and one you can't view a `403`, whatever `format` says. The `format` on a `POST` or `PATCH` only chooses the format of the response. You can send Markdown and get JSON back, or the other way round. It's normally a query parameter, but a `format` field in the body works too. ### When Markdown isn't available Folioso never answers with Markdown that wouldn't read back to the stored page (see [The Markdown round trip](#the-markdown-round-trip)). Almost every page can be written that way, if need be with [JSON blocks](/api/markdown-extensions#json-blocks). A page whose stored JSON isn't a well-formed document can't, for example one written through the API with an extra key beside `type` and `content`, a top-level `type` other than `doc`, or a `content` that isn't a list. - **Reads** (`GET /pages/{slug}`, `GET /recycle-bin/{slug}` and `GET /pages/{slug}/snapshots/{id}`) with `?format=markdown` answer `422` on `format`: ```json { "message": "This page can't be represented as Markdown without loss; read it with format=json.", "errors": { "format": ["This page can't be represented as Markdown without loss; read it with format=json."] } } ``` - **Writes** (`POST /pages` and `PATCH /pages/{slug}`) with `?format=markdown` have already saved the page by the time the response is written, so they don't answer `422`. They succeed as usual and answer in the JSON shape instead: `content` and `contentEpoch`, with no `markdown` key. Check for `markdown` before reading it. The same page can always be read with `format=json`. ## Writing a body Send the body in **one** of two fields on `POST /pages` or `PATCH /pages/{slug}`: - `content`: a TipTap JSON document, or `null`. - `markdown`: a Markdown string, or `null`. Sending both is refused with a `422`, whatever their values, even `"content": null` alongside `"markdown": ""`: ```json { "message": "Send the page body as content or as markdown, not both.", "errors": { "markdown": ["Send the page body as content or as markdown, not both."] } } ``` A `markdown` value that isn't a string or `null` is a `422` too. ### Empty and blank bodies | You send | The page stores | `GET` returns as JSON | `GET` returns as Markdown | | --- | --- | --- | --- | | Neither field, on `PATCH` | The body is unchanged. | | | | Neither field, on `POST` | No body. | `"content": null` | `"markdown": null` | | `"content": null` | No body. | `"content": null` | `"markdown": null` | | `"markdown": null` or `"markdown": ""` | No body, exactly as `"content": null`. | `"content": null` | `"markdown": null` | | Markdown that's only spaces or blank lines | One empty paragraph. | `{"type":"doc","content":[{"type":"paragraph"}]}` | `[](folioso:attrs)` | | `"markdown": "[](folioso:empty)"` | A document with no blocks. | `{"type":"doc","content":[]}` | `[](folioso:empty)` | > [!WARNING] > On a `PATCH`, leaving both fields out keeps the body, but `"markdown": ""` empties the page, just as `"content": null` does. Markdown isn't trimmed. Spaces, non-breaking spaces and zero-width characters at its start and end are kept, so a body read as Markdown and sent back loses nothing at its edges. The `title` is still trimmed. ### When Markdown is refused A `folioso:` extension written wrongly, or a ` ```folioso ` block that doesn't hold valid JSON, is a `422` on `markdown`, with a message saying what's wrong: ```json { "message": "A folioso:page link takes an id and an optional slug, and nothing else.", "errors": { "markdown": ["A folioso:page link takes an id and an optional slug, and nothing else."] } } ``` Nothing is changed when this happens, and a `POST` doesn't create the page. Ordinary Markdown is never refused: anything that isn't a Folioso extension is read as Markdown, however unusual. [Folioso Markdown extensions](/api/markdown-extensions#error-messages) lists every message. ## Content writes and people editing the page A body sent as Markdown is stored exactly as if you'd sent the resulting JSON as `content`. Everything that follows from a content write applies to both: - **`PATCH`:** if the body you send differs from the stored one, the page gets a new `contentEpoch`, which the response returns, and anyone with the page open in the editor is reloaded with your content. Sending back the body exactly as stored changes neither. Send the `contentEpoch` you last read as `epoch` to be refused with `409 Conflict` if the page was restored or replaced since. See [Avoid overwriting a restore](/api/pages#avoid-overwriting-a-restore). - **`POST`:** a new page starts at `contentEpoch` 0. There's nothing to conflict with, so `epoch` isn't used. - **Both:** people [mentioned](/api/tiptap-json#mention) in the body are notified, [page links](/api/tiptap-json#pagelink) update the target's **Linked from** list, and the page is re-indexed for search, as when you edit in the app. ## The Markdown round trip **Markdown read with `?format=markdown` and sent back unchanged as `markdown` stores the same document.** Page links keep their target, mentions keep the person, and alert boxes, image sizes, alignment, table column widths, code languages, diagrams, underline and every other attribute survive. You can read a page as Markdown, change part of it, and write it back without losing anything you didn't change. ### What "the same document" means The stored JSON is the same document the editor would load. It may differ from the original only in details the editor itself evens out every time it opens a page: 1. **Default attributes.** An attribute at its default value may be left out, or filled in. `{"type":"paragraph"}` and `{"type":"paragraph","attrs":{"textAlign":null}}` are the same paragraph. [TipTap JSON reference](/api/tiptap-json) lists every default. 2. **Split text.** Neighbouring text nodes with the same marks may be joined into one. 3. **Mark order.** Marks on a text node are kept in a fixed order: `link`, `bold`, `code`, `italic`, `strike`, `underline`. 4. **Missing content.** A node with no `content` key is the same as one with `"content": []`. One attribute is never filled in: a code block's `language`. Its default is each reader's own preferred language, so a code block with no `language` key at all is kept that way, and stays different from one whose `language` is `null`. Values keep their JSON types. A mention whose `id` is the number `5` and one whose `id` is the string `"5"` stay different. ### How Folioso keeps it exact Folioso writes each top-level block as ordinary Markdown wherever that reads back exactly, using a [Folioso extension](/api/markdown-extensions) where plain Markdown has no syntax, such as `[text](folioso:span?marks=underline)` for underlined text. Before answering, it reads its own Markdown back and compares the result with the stored page. Any block that wouldn't come back exactly is written instead as a ` ```folioso ` block holding that block's TipTap JSON, as stored: ````markdown ## Install ```folioso {"type":"orderedList","attrs":{"start":1,"type":"a"},"content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Lettered"}]}]}]} ``` ```` Most pages have no ` ```folioso ` blocks. They appear for what Markdown can't carry, such as a lettered list, a table with merged cells, or a link with extra attributes. See [JSON blocks](/api/markdown-extensions#json-blocks). ### Editing Markdown you've read - **Change anything.** Whatever you edit is read with the ordinary [Markdown rules](/api/markdown), and the rest comes back as it was. - **Leave the Folioso extensions alone** unless you mean to change them. Deleting `(folioso:span?marks=underline)` from a link removes the underline, for example. - **Keep ` ```folioso ` blocks valid.** Each must still hold exactly one JSON object with a string `type`, or the whole request is refused with a `422`. You can also replace one with ordinary Markdown. - **Link labels are for reading only.** A page link's text is the target's title as you see it, or `Restricted page` if you can't open it. Folioso ignores it when you send the Markdown back, so a page link keeps its target whatever its text says. --- # Writing Markdown Send a page body as Markdown in the `markdown` field and Folioso converts it into the same content the editor makes. This page lists every piece of Markdown that's understood, what each becomes on the page, and what plain Markdown can't express. Folioso reads [CommonMark](https://commonmark.org) with three GitHub additions: tables, task lists and strikethrough. On top of that it understands its own [Folioso Markdown extensions](/api/markdown-extensions), which carry the things Markdown has no syntax for. You never need them for ordinary writing. Each example below shows the Markdown you send and the [TipTap JSON](/api/tiptap-json) the page then stores. To see how a page you already have reads as Markdown, fetch it with `?format=markdown`. See [Content formats](/api/content-formats). ## Paragraphs and line breaks Separate paragraphs with a blank line. A single line break inside a paragraph becomes a space. To keep a line break, end the line with a backslash or with two spaces: ```markdown First line\ second line and the same line. ``` ```json { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "First line" }, { "type": "hardBreak" }, { "type": "text", "text": "second line and the same line." } ] } ] } ``` A backslash at the very end of a paragraph is kept as a backslash, as CommonMark says. ## Headings Both `#` headings, levels 1 to 6, and underlined headings work: ```markdown ## Install Upgrading --------- ``` ```json { "type": "doc", "content": [ { "type": "heading", "attrs": { "level": 2 }, "content": [{ "type": "text", "text": "Install" }] }, { "type": "heading", "attrs": { "level": 2 }, "content": [{ "type": "text", "text": "Upgrading" }] } ] } ``` > [!TIP] > A line of `---` straight after a line of text turns that text into a heading. Leave a blank line before `---` when you mean a horizontal rule. ## Bold, italic, strikethrough and code ```markdown **Bold**, *italic*, ~~struck~~ and `inline code`. ``` ```json { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "Bold", "marks": [{ "type": "bold" }] }, { "type": "text", "text": ", " }, { "type": "text", "text": "italic", "marks": [{ "type": "italic" }] }, { "type": "text", "text": ", " }, { "type": "text", "text": "struck", "marks": [{ "type": "strike" }] }, { "type": "text", "text": " and " }, { "type": "text", "text": "inline code", "marks": [{ "type": "code" }] }, { "type": "text", "text": "." } ] } ] } ``` - `__bold__` and `_italic_` work too, and so does a single `~` for strikethrough. - Marks combine: `***bold italic***` is both. - Inline code can't carry other marks. To show a backtick inside it, use a longer run of backticks around it: ``` `` a ` b `` ```. - Markdown has no underline. Use a [mark span](/api/markdown-extensions#mark-spans): `[text](folioso:span?marks=underline)`. ## Links Inline links, reference links and links in angle brackets all work: ```markdown See [the guide](https://example.com/guide "The guide") or . Read the [release notes][notes]. [notes]: https://example.com/notes ``` ```json { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "See " }, { "type": "text", "text": "the guide", "marks": [ { "type": "link", "attrs": { "href": "https://example.com/guide", "target": "_blank", "rel": "noopener noreferrer", "class": null, "title": "The guide" } } ] }, { "type": "text", "text": " or " }, { "type": "text", "text": "https://example.com", "marks": [ { "type": "link", "attrs": { "href": "https://example.com", "target": "_blank", "rel": "noopener noreferrer", "class": null, "title": null } } ] }, { "type": "text", "text": "." } ] }, { "type": "paragraph", "content": [ { "type": "text", "text": "Read the " }, { "type": "text", "text": "release notes", "marks": [ { "type": "link", "attrs": { "href": "https://example.com/notes", "target": "_blank", "rel": "noopener noreferrer", "class": null, "title": null } } ] }, { "type": "text", "text": "." } ] } ] } ``` Links get the same attributes as links added in the editor: they open in a new tab with `rel="noopener noreferrer"`. Inline code can't carry a link in the editor, so any part of a link's text written as inline code loses the link. A link whose whole text is inline code, such as ``[`composer install`](https://getcomposer.org)``, becomes plain inline code with no link at all. ### Which addresses become links In Markdown you write by hand, a link is kept only when its address: - starts with `http:`, `https:` or `mailto:`, in any letter case; - starts with `/`, such as `/engineering/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b`; or - starts with `#`. An address starting with `//`, such as `//example.com/page`, also starts with `/`, so it's kept. Browsers read it as a link to another site, using the page's own `https:`. The same goes for images. Any other address, such as `javascript:`, `ftp:` or a bare relative path like `notes.md`, is dropped and its text is kept as plain text: ```markdown [Run me](javascript:alert(1)) ``` ```json { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Run me" }] }] } ``` This rule applies only to Markdown links and images. Links inside a [JSON block](/api/markdown-extensions#json-blocks), and links sent as TipTap JSON in `content`, are stored exactly as given, as `content` always has been. ### Bare addresses stay text An address typed without `<…>` around it, such as `https://example.com`, isn't turned into a link. Put it in angle brackets, or use `[text](address)`, to link it. To link to another Folioso page so that its title stays up to date and it shows in **Linked from**, use a [page link](/api/markdown-extensions#page-links) rather than an ordinary link to its address. ## Images An image becomes an image block of its own. The editor's images are blocks, so an image in the middle of a paragraph splits the paragraph around it: ```markdown Before ![Architecture diagram](https://example.com/architecture.png "Overview") after. ``` ```json { "type": "doc", "content": [ { "type": "paragraph", "content": [{ "type": "text", "text": "Before " }] }, { "type": "image", "attrs": { "src": "https://example.com/architecture.png", "alt": "Architecture diagram", "title": "Overview" } }, { "type": "paragraph", "content": [{ "type": "text", "text": " after." }] } ] } ``` - The image's text is its alt text, which the editor shows as the caption. - An image's address must start with `http://`, `https://` or `/`. Otherwise, including a `data:` address, the image is dropped and its alt text is kept as plain text. - An image inside a link keeps the image but not the link. - To upload an image first and then use it, see [Images](/api/images). - To set an image's width, alignment or other attributes, use a [block attributes](/api/markdown-extensions#block-attributes) carrier. ## Lists Bulleted lists use `-`, `*` or `+`. Numbered lists use `1.` or `1)`, and start at the first item's number: ```markdown 3. Three 4. Four - Nested bullet ``` ```json { "type": "doc", "content": [ { "type": "orderedList", "attrs": { "start": 3 }, "content": [ { "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Three" }] }] }, { "type": "listItem", "content": [ { "type": "paragraph", "content": [{ "type": "text", "text": "Four" }] }, { "type": "bulletList", "content": [ { "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Nested bullet" }] }] } ] } ] } ] } ] } ``` - Changing the bullet character, or switching between `.` and `)`, starts a new list. `- a` followed by `* b` is two lists. - A list item can hold more blocks, such as code blocks or further paragraphs, indented to line up with the item's text. - An item always starts with a paragraph, so an item that starts with something else, or is empty, gets an empty paragraph first. ## Task lists A **bulleted** list (`-`, `*` or `+`) in which **every** item starts with `[ ]` or `[x]`, followed by a space and the item's text, becomes a task list: ```markdown - [x] Write the migration - [ ] Deploy ``` ```json { "type": "doc", "content": [ { "type": "taskList", "content": [ { "type": "taskItem", "attrs": { "checked": true }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Write the migration" }] }] }, { "type": "taskItem", "attrs": { "checked": false }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Deploy" }] }] } ] } ] } ``` If only some items have a checkbox, the list stays an ordinary bulleted list and the checkboxes are kept as the text `[ ]` or `[x]`. Task items can be nested, and can hold other blocks, like ordinary list items. - A numbered list is never a task list: `1. [ ] Deploy` keeps `[ ]` as text. - A checkbox with nothing after it, such as `- [ ]` on its own, is an ordinary list item holding the text `[ ]`. To write an empty task item, add a [block attributes](/api/markdown-extensions#empty-paragraphs) carrier: `- [ ] [](folioso:attrs)`. - `[X]` is accepted as ticked. Folioso writes it back as `[x]`. ## Quotes ```markdown > Quoted text > > - and a list ``` ```json { "type": "doc", "content": [ { "type": "blockquote", "content": [ { "type": "paragraph", "content": [{ "type": "text", "text": "Quoted text" }] }, { "type": "bulletList", "content": [{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "and a list" }] }] }] } ] } ] } ``` A quote can hold any blocks, including other quotes. An empty quote gets one empty paragraph. ## Alert boxes A quote whose first line starts with `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]` or `[!CAUTION]` becomes an [alert box](/editor/alerts). The rest of that first line is the box's title, and the paragraphs after it are its body: ```markdown > [!WARNING] Back up **first** > This deletes the staging database. ``` ```json { "type": "doc", "content": [ { "type": "alertBox", "attrs": { "type": "warning" }, "content": [ { "type": "alertTitle", "content": [ { "type": "text", "text": "Back up " }, { "type": "text", "text": "first", "marks": [{ "type": "bold" }] } ] }, { "type": "alertBody", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "This deletes the staging database." }] }] } ] } ] } ``` | You write | Alert box type | | --- | --- | | `[!NOTE]` | Info (`info`) | | `[!IMPORTANT]` | Info (`info`) | | `[!TIP]` | Success (`success`) | | `[!WARNING]` | Warning (`warning`) | | `[!CAUTION]` | Error (`error`) | - The type is matched in any letter case, and must be followed by a space or the end of the line. - The title can hold bold, italic, code and the other marks. It can be left out, as in GitHub's form, where `[!NOTE]` is alone on its line. - The body can hold only paragraphs. Lists, code blocks, headings and images in the quote are moved out and placed straight after the box, in order. A box with no body gets one empty paragraph. - Alert boxes exist only at the top level of a page. `[!NOTE]` in a quote inside a list, or in a quote inside another quote, is an ordinary quote that starts with the text `[!NOTE]`. So is any other word in the brackets, such as `[!INFO]`. ## Code blocks Fence code with three or more backticks or tildes. The word after the fence is the code block's language: ````markdown ```js const answer = 42; ``` ```` ```json { "type": "doc", "content": [ { "type": "codeBlock", "attrs": { "language": "javascript" }, "content": [{ "type": "text", "text": "const answer = 42;" }] } ] } ``` The language is worked out the way the editor does when you type a fence: 1. Only the first word counts, lower-cased: ` ```PHP ` gives `php`. 2. It's kept only if it's made of letters, digits and `_ + # -`. Otherwise the block has no language. 3. Common short names are expanded: | You write | Language stored | | --- | --- | | `js` | `javascript` | | `ts` | `typescript` | | `py` | `python` | | `sh`, `shell`, `zsh`, `shellscript` | `bash` | | `yml` | `yaml` | | `docker` | `dockerfile` | A fence with no language, and a block indented by four spaces, give a code block whose `language` is `null`, which the editor shows as **Plain text**. Any other language is kept as written, as in the editor. See [Code blocks](/editor/code-blocks#choose-the-language). To store a language exactly as written, with no lower-casing or short names, add the word `verbatim` after it, as in ` ```js verbatim `. See [Exact code languages](/api/markdown-extensions#exact-code-languages). To put three backticks inside a code block, fence it with four or more. ## Diagrams A fence whose language is `mermaid` or `plantuml`, in any letter case, becomes a [diagram](/editor/diagrams) rather than a code block: ````markdown ```mermaid graph TD; A[Start] --> B[End]; ``` ```` ```json { "type": "doc", "content": [ { "type": "mermaid", "attrs": { "code": "graph TD;\n A[Start] --> B[End];", "engine": "mermaid" } } ] } ``` For PlantUML, write ` ```plantuml ` and include the `@startuml` and `@enduml` lines in the source. To keep Mermaid or PlantUML source as an ordinary code block instead, write ` ```mermaid verbatim `. ## Tables GitHub pipe tables become tables. The first row is the header row: ```markdown | Setting | Value | | :--- | ---: | | `timeout` | 30 \| 60 | ``` ```json { "type": "doc", "content": [ { "type": "table", "content": [ { "type": "tableRow", "content": [ { "type": "tableHeader", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Setting" }] }] }, { "type": "tableHeader", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Value" }] }] } ] }, { "type": "tableRow", "content": [ { "type": "tableCell", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "timeout", "marks": [{ "type": "code" }] }] }] }, { "type": "tableCell", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "30 | 60" }] }] } ] } ] } ] } ``` - Write `\|` for a `|` inside a cell. - The colons that align columns are accepted but ignored. The editor has no column alignment. - Each cell holds one line. HTML such as `
` is kept as text. Table cells in the editor can hold several paragraphs, lists and more; to write those, use a [JSON block](/api/markdown-extensions#json-blocks). - To set a column's width, use a [block attributes](/api/markdown-extensions#block-attributes) carrier at the start of a cell. ## Horizontal rules `---`, `***` or `___` on a line of their own become a horizontal rule. Directly under a line of text, `---` makes that text a level 2 heading instead, so put a blank line before it. `***` and `___` are rules either way. ## HTML HTML is never interpreted. HTML tags are kept as plain text, exactly as typed, so `Press Enter` shows the tags on the page. A block of HTML becomes a paragraph of its lines, with line breaks between them. ## Escapes and character references A backslash before a punctuation character keeps it as that character: `\*not italic\*` is the text `*not italic*`. Character references such as `&`, `©` and `#` become the characters they name. ## What plain Markdown can't express Some of what the editor can store has no plain Markdown syntax. The [Folioso Markdown extensions](/api/markdown-extensions) carry each of these: | To write | Use | | --- | --- | | A link to another Folioso page | A [page link](/api/markdown-extensions#page-links) | | A mention of a person | A [mention](/api/markdown-extensions#mentions) | | Underlined text | A [mark span](/api/markdown-extensions#mark-spans) | | Paragraph, heading or image alignment | [Block attributes](/api/markdown-extensions#block-attributes) | | An image's width and height | [Block attributes](/api/markdown-extensions#block-attributes) | | A table column's width | [Block attributes](/api/markdown-extensions#block-attributes) | | An empty paragraph or list item | [Block attributes](/api/markdown-extensions#block-attributes) with nothing set | | A page with no blocks at all | [`[](folioso:empty)`](/api/markdown-extensions#empty-document) | | A code block language exactly as written | [`verbatim`](/api/markdown-extensions#exact-code-languages) | | Anything else: lettered lists, merged table cells, cells with several blocks, headings with line breaks, links with other attributes | A [JSON block](/api/markdown-extensions#json-blocks) | Markdown features the editor has no equivalent for aren't supported, and are read as the CommonMark they are: - **Footnotes:** `[^1]` isn't a footnote. - **Front matter:** a `---` block at the top of the text isn't metadata. It reads as a horizontal rule and a heading. - **Heading ids, definition lists, maths and emoji codes** stay as the text you typed. > [!WARNING] > A footnote definition whose text is a single word or address, such as `[^1]: source` or `[^1]: https://example.com/paper`, is read as a Markdown link reference definition. The definition line **disappears, and its text is lost**: `[^1]` then reads as `^1`, or as a link to that address. Write footnotes as ordinary text, such as a numbered list at the end of the page, before sending them. --- # Folioso Markdown extensions Folioso Markdown is ordinary Markdown plus a few extensions that carry what plain Markdown can't: page links, mentions, underline, alignment and other attributes, and any block with no Markdown form at all. This page is the reference for each extension, how Folioso writes Markdown when you read a page, and every error the extensions can produce. None of the extensions is HTML, and none can be triggered by accident. Each is one of three ordinary Markdown constructs: | Construct | Example | Used for | | --- | --- | --- | | A link whose address uses the reserved `folioso:` scheme | `[Deploy runbook](folioso:page?id=42&slug=…)` | [Page links](#page-links), [mentions](#mentions), [mark spans](#mark-spans), [block attributes](#block-attributes) and the [empty document](#empty-document) | | The word `verbatim` after a code fence's language | ` ```js verbatim ` | [Exact code languages](#exact-code-languages) | | A fenced block whose language is `folioso` | ` ```folioso ` | [JSON blocks](#json-blocks) | You only need these to write what plain Markdown can't express. When you read a page with `?format=markdown`, Folioso uses them for you, and sending that Markdown back unchanged keeps everything. See [Content formats](/api/content-formats#the-markdown-round-trip). ## Parameters A `folioso:` link takes its parameters as a query string after a `?`, the way a web address does: ```text folioso:KIND?name=value&name=value ``` - **Encoding.** Names and values are percent-decoded: `%20` is a space, `%26` an `&`, `%3D` an `=` and `%23` a `#`. A `+` is a plain `+`, not a space. Characters that can't appear in a Markdown link address, such as spaces, must be percent-encoded, or the whole address wrapped in `<…>`. Otherwise Markdown doesn't see a link at all, and the carrier is kept on the page as plain text. - **Typed values.** A value that's valid JSON takes that JSON value. Anything else is a string. - **Empty pairs** (`&&`) are ignored. A pair with no `=` is refused. When a name is repeated, the last value wins. | Value as written | Value stored | | --- | --- | | `id=42` | the number `42` | | `id=%2242%22` or `id="42"` | the string `"42"` | | `textAlign=center` | the string `"center"` | | `alt=null` | `null` | | `checked=true` | `true` | | `colwidth=%5B240%5D` or `colwidth=[240]` | the array `[240]` | | `label=Ada%20Lovelace` | the string `"Ada Lovelace"` | So to send a string that would otherwise read as JSON, such as `"42"`, `"true"` or `"null"`, put it in double quotes. Folioso does this itself when it writes a parameter: every value that isn't a string, and every string that would read as JSON, is written as JSON. ## Page links A link to another Folioso page, which the editor shows with the page's current title and lists in the target's **Linked from**. See [Page links and mentions](/editor/links-and-mentions). ```text [label](folioso:page?id=ID&slug=SLUG) ``` | Parameter | Required | Description | | --- | --- | --- | | `id` | Yes | The target page's numeric id, as stored in its `pageLink` nodes. Usually a number. | | `slug` | No | The target page's slug. Include it, as the editor does: the editor uses it to open the page. | ```markdown Follow [Deploy runbook](folioso:page?id=42&slug=9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b) before releasing. ``` ```json { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "Follow " }, { "type": "pageLink", "attrs": { "id": 42, "slug": "9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b" } }, { "type": "text", "text": " before releasing." } ] } ] } ``` - **The label is ignored** when you send it. The page always shows the target's current title, so you can write anything between the brackets. - **When Folioso writes a page link**, the label is the target's title as **you** see it, `Untitled` for an untitled page, or `Restricted page` when you can't open the target. A title you can't see is never shown. - **Nothing is looked up** when you send one. A page link is stored as given, exactly as it would be from `content`. A target that doesn't exist, or that you can't open, isn't an error: the editor shows **Restricted page** to anyone who can't open it. - Bold, italic and the other marks around a page link apply to it: `**[x](folioso:page?id=42&slug=…)**` is a bold page link. - Any parameter other than `id` and `slug`, or a missing `id`, is refused. Every page object the API returns carries the page's numeric `id`, so take it from [Get a page](/api/pages#get-a-page), the [page list](/api/pages#list-pages) or a search result. ## Mentions A mention of a person, which notifies them as a mention typed in the editor would. ```text [@Label](folioso:mention?id=ID) ``` | Parameter | Required | Description | | --- | --- | --- | | `id` | Yes | The person's user id, as stored in `mention` nodes. | | `char` | No | The character the mention starts with. Leave it out for the usual `@`. | | `taggedBy` | No | The user id of whoever added the mention. | | `label` | No | The label, when it isn't the link text. Folioso writes this only for a label that isn't a string. | ```markdown Ask [@Ada Lovelace](folioso:mention?id=5) to review. ``` ```json { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "Ask " }, { "type": "mention", "attrs": { "id": 5, "label": "Ada Lovelace" } }, { "type": "text", "text": " to review." } ] } ] } ``` - **The label** is the link text without its first character, which must be the mention character: `@`, or the `char` you give. `[Ada](folioso:mention?id=5)` is refused because it doesn't start with `@`. With a `label` parameter, the link text isn't checked or used. - **The label is display text.** The mention is the person with that `id`. Whether they're notified follows the same rules as the editor. See [Who you can mention](/editor/links-and-mentions#who-you-can-mention). - **Nothing is looked up.** The mention is stored as given. - Marks around a mention apply to it, as for page links. Your own user id is `data.user.id` in [`GET /me`](/api/overview#your-first-request), so you can mention yourself or record yourself as `taggedBy`. The API doesn't list other people's ids: the only place to find one is an existing `mention` node in content you can read. ## Mark spans Applies marks to a run of text. Use it for underline, which Markdown has no syntax for, and for bold, italic or strikethrough where `**`, `*` or `~~` wouldn't work, such as around text that starts or ends with punctuation or a space. ```text [text](folioso:span?marks=MARK,MARK) ``` | Parameter | Required | Description | | --- | --- | --- | | `marks` | Yes | A comma-separated list of `bold`, `italic`, `strike` and `underline`, with no spaces. | ```markdown This is [important](folioso:span?marks=underline) and [hello, ](folioso:span?marks=bold)world. ``` ```json { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "This is " }, { "type": "text", "text": "important", "marks": [{ "type": "underline" }] }, { "type": "text", "text": " and " }, { "type": "text", "text": "hello, ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "world." } ] } ] } ``` - The text inside a span can hold other Markdown, such as `*italic*`, and the span's marks are added to it. Inline code inside a span stays plain inline code, because code can't carry other marks. - Links and inline code can't be listed in a span. Markdown doesn't allow a link inside a link, so a span can't hold a link, page link or mention, and can't go inside a link. Folioso writes underlined link text, and a page link or mention with marks, as a [JSON block](#json-blocks). - Any other parameter, or a mark not in the list, is refused. ## Block attributes Sets attributes on the block its line belongs to, for what Markdown can't say: alignment, image size, a missing alt text, a table column's width, or nothing at all, to make an empty paragraph. ```text [](folioso:attrs?name=value&name=value) ``` The carrier must have an **empty label**, `[]`, and must be the **first thing in its paragraph, heading or table cell**. Each parameter becomes an attribute of that name with its [typed value](#parameters). Where it applies depends on where it sits: | Position | Applies to | | --- | --- | | At the start of a paragraph | The paragraph | | After the `#` marks of a heading | The heading | | At the start of a line holding only an image | The image | | At the start of a table cell | The cell, not the paragraph inside it | | Alone on a line | An empty paragraph, with any attributes given | ### Alignment `textAlign` is `center` or `right`. Left is the default, stored as `null`, so it's never written. `left` is accepted too, and stored as `"left"`. Any other value is refused. ```markdown [](folioso:attrs?textAlign=center)A centred paragraph. ## [](folioso:attrs?textAlign=right)A right-aligned heading ``` ```json { "type": "doc", "content": [ { "type": "paragraph", "attrs": { "textAlign": "center" }, "content": [{ "type": "text", "text": "A centred paragraph." }] }, { "type": "heading", "attrs": { "level": 2, "textAlign": "right" }, "content": [{ "type": "text", "text": "A right-aligned heading" }] } ] } ``` ### Image size and alt text `width` and `height` are whole numbers of pixels. The editor's resize handles set `width`. `textAlign` places the image. `alt=null` stores no alt text at all, which `![](…)` can't, because it reads as an empty alt text. ```markdown [](folioso:attrs?width=480&textAlign=center)![Architecture](https://example.com/architecture.png) ``` ```json { "type": "doc", "content": [ { "type": "image", "attrs": { "src": "https://example.com/architecture.png", "alt": "Architecture", "title": null, "width": 480, "textAlign": "center" } } ] } ``` ### Column widths At the start of a table cell, the carrier sets the cell's attributes. `colwidth` is an array holding the column's width in pixels. The editor sets it on every cell in a column you resize: ```markdown | [](folioso:attrs?colwidth=[240])Name | Value | | --- | --- | | [](folioso:attrs?colwidth=[240])timeout | 30 | ``` ```json { "type": "doc", "content": [ { "type": "table", "content": [ { "type": "tableRow", "content": [ { "type": "tableHeader", "attrs": { "colwidth": [240] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Name" }] }] }, { "type": "tableHeader", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Value" }] }] } ] }, { "type": "tableRow", "content": [ { "type": "tableCell", "attrs": { "colwidth": [240] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "timeout" }] }] }, { "type": "tableCell", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "30" }] }] } ] } ] } ] } ``` Because a cell's carrier sets the cell, a paragraph inside a cell can't be aligned in Markdown. Folioso writes such a table as a [JSON block](#json-blocks). ### Empty paragraphs Markdown drops empty paragraphs. A carrier with no parameters, on a line of its own, keeps one. It works anywhere a paragraph can go, such as an empty list item or task item: ```markdown Above [](folioso:attrs) - [ ] [](folioso:attrs) ``` ```json { "type": "doc", "content": [ { "type": "paragraph", "content": [{ "type": "text", "text": "Above" }] }, { "type": "paragraph" }, { "type": "taskList", "content": [{ "type": "taskItem", "attrs": { "checked": false }, "content": [{ "type": "paragraph" }] }] } ] } ``` ### Rules - Only the attributes the block has in the [TipTap JSON reference](/api/tiptap-json) can be set: a paragraph's `textAlign`, a heading's `textAlign`, an image's `textAlign`, `alt`, `title`, `width` and `height`, and a table cell's `colspan`, `rowspan`, `colwidth` and `align`. Any other name is refused, and so are an image's `src` and a heading's `level`, which the Markdown itself gives. - `textAlign` must be `left`, `center` or `right`. - A carrier with text inside its brackets, such as `[x](folioso:attrs?…)`, is refused. - A carrier anywhere but the start of its paragraph, heading or table cell is refused. That includes the start of a second line of a paragraph, because the paragraph began on the line before. - A carrier on a line that splits into more than one block is refused. That happens when the line holds an image as well as text, because the image becomes a block of its own. ## Empty document A page whose document has no blocks at all: ```markdown [](folioso:empty) ``` ```json { "type": "doc", "content": [] } ``` It must be the whole of the Markdown. Anywhere else it's refused. It isn't the same as an empty page: sending `"markdown": ""` or `null` removes the body altogether. See [Empty and blank bodies](/api/content-formats#empty-and-blank-bodies). ## Exact code languages Add the word `verbatim` after a code fence's language to store the language exactly as written, without the lower-casing and short names [ordinary fences](/api/markdown#code-blocks) get: ````markdown ```JS verbatim let b = 2; ``` ```` ```json { "type": "doc", "content": [{ "type": "codeBlock", "attrs": { "language": "JS" }, "content": [{ "type": "text", "text": "let b = 2;" }] }] } ``` - Everything in the info string before ` verbatim` is the language, spaces included, so ` ```c sharp verbatim ` stores `c sharp`. - `verbatim` counts only as the last of at least two words. A fence of just ` ```verbatim ` is a code block whose language is `verbatim`. - With `verbatim`, ` ```mermaid ` and ` ```plantuml ` stay code blocks rather than becoming diagrams, and ` ```folioso ` is an ordinary code block rather than a JSON block. Folioso writes `verbatim` whenever an ordinary fence wouldn't give back the stored language. For example, a block stored with language `js` is written ` ```js verbatim `, because ` ```js ` alone would become `javascript`. ## JSON blocks A fenced block whose language is `folioso` holds one top-level block as TipTap JSON. It's how anything without a Markdown form travels through Markdown unchanged. `````markdown Introduction. ```folioso {"type":"orderedList","attrs":{"start":1,"type":"a"},"content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Lettered"}]}]}]} ``` ````` ```json { "type": "doc", "content": [ { "type": "paragraph", "content": [{ "type": "text", "text": "Introduction." }] }, { "type": "orderedList", "attrs": { "start": 1, "type": "a" }, "content": [{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Lettered" }] }] }] } ] } ``` ### Writing a JSON block - The info string is exactly `folioso`, in lower case, with nothing after it. ` ```folioso extra ` or ` ```FOLIOSO ` is an ordinary code block. - The block holds **one** JSON object with a string `type`: a single TipTap node, as described in the [TipTap JSON reference](/api/tiptap-json). An array, a bare value, or JSON that doesn't parse is refused. - The node is stored exactly as written. Folioso doesn't check its type or attributes, as with `content`. Links and image addresses inside it aren't limited to the [addresses ordinary Markdown allows](/api/markdown#which-addresses-become-links). - Use one JSON block per top-level block, at the top level of the Markdown. Inside a list or a quote, a JSON block becomes a child of that list item or quote. - The JSON can span several lines and be indented however you like. - Fence it with more backticks than the longest run of backticks inside the JSON, and at least three. ### When Folioso writes a JSON block When you read a page as Markdown, a top-level block is written as a JSON block when its Markdown form wouldn't read back exactly. The block is written as it's stored, on one line. That includes: - a numbered list with a `type`, such as a lettered or Roman-numeral list; - a table with merged cells, cells holding more than one paragraph or a line break, header cells outside the first row, no header row, or an aligned paragraph in a cell; - a heading containing a line break; - a link with attributes other than the editor's defaults, a link title of `""`, or underline on link text; - a page link or mention that carries marks, such as a bold mention; - inline code containing a line break; - a code block with no `language` attribute at all; - an image whose address isn't one [Markdown allows](/api/markdown#which-addresses-become-links), or contains a line break; - a node or mark Folioso doesn't know, such as content written by a newer version of the editor; - an attribute the node doesn't have, on any block, or a `textAlign` other than `left`, `center` or `right`. A [block attributes](#block-attributes) carrier only carries the attributes listed in its rules, so anything else on a paragraph, heading, image or cell makes its block a JSON block too. Only the block that needs it becomes JSON. The rest of the page stays readable. Occasionally the block before it does too, when the two would otherwise run together as Markdown. ## How Folioso writes Markdown When you read a page with `?format=markdown`, Folioso writes the Markdown in a fixed way, so that reading it back gives the stored document. You can write it the same way by hand, but you don't need to: any Markdown that means the same thing works. | Content | Written as | | --- | --- | | Blocks | Separated by one blank line. | | Paragraph | Its text, escaped (below). An empty paragraph is `[](folioso:attrs)`. | | Heading | `#` to `######`, a space, then its text. | | Line break | A backslash at the end of the line. | | Bold, italic, strikethrough | `**…**`, `*…*`, `~~…~~` when the text inside starts and ends with a letter or digit, otherwise a [mark span](#mark-spans). | | Underline | A [mark span](#mark-spans). | | Inline code | Backticks, more than the longest run inside, with a space inside each end when the code starts or ends with a backtick or space. | | Link | `[text](
"title")`, the address always in angle brackets. | | Image | `![alt](
"title")` on its own line, after a [block attributes](#block-attributes) carrier when needed. | | Bulleted list | `-`, or `*` when the block before it also used `-`, so that two lists side by side stay two lists. | | Numbered list | Numbered from its `start`, with `.`, or `)` when the block before it also used `.`. | | Task list | `- [ ]` and `- [x]`, alternating bullets as above. An empty item is `- [ ] [](folioso:attrs)`. | | Nested blocks | Indented to line up with the item's text. | | Quote | `> ` before every line. | | Alert box | `> [!NOTE]`, `[!TIP]`, `[!WARNING]` or `[!CAUTION]` and the title on the first line, then each body paragraph after a `>` line. | | Code block | A backtick fence longer than any run of backticks in the code, the stored language, and ` verbatim` when needed. | | Diagram | A ` ```mermaid ` or ` ```plantuml ` fence holding its source. | | Table | A pipe table, when it has a header row and nothing a pipe table can't hold. Otherwise a [JSON block](#json-blocks). | | Horizontal rule | `---` | | Page link, mention | As in [Page links](#page-links) and [Mentions](#mentions). | | Empty document | `[](folioso:empty)` | ### Escaping So that text reads back as text, Folioso escapes it as it writes: - These characters always get a backslash before them: `` \ ` * _ [ ] < > ~ | & # ``. - At the start of a line, so the line can't become a list, heading or other block, it also escapes a leading `+`, `-` or `=`, and the `.` or `)` after a leading number: `1\. not a list`. - A `!` at the end of a run of text gets a backslash, so it can't combine with a following `[` into an image. - Spaces, tabs, non-breaking spaces and other blank characters at the start or end of a line are written as character references, such as ` ` for a space, because Markdown would otherwise drop them. - A line break stored inside text, rather than as a line break node, is written as ` `, a carriage return as ` `, and the invisible character U+FEFF always as ``. ```json { "type": "paragraph", "content": [{ "type": "text", "text": "Cost: 5 * 3 = 15 #1 [x] a|b & c\\d" }] } ``` ```markdown Cost: 5 \* 3 = 15 \#1 \ \[x\] a\|b \& c\\d ``` Bare web addresses in text need no escaping, because they aren't turned into links. See [Bare addresses stay text](/api/markdown#bare-addresses-stay-text). ## Error messages A malformed extension makes the whole request a `422`, with the message under `errors.markdown`. Nothing is saved. | Message | Cause | | --- | --- | | `Unknown carrier folioso:KIND.` | A `folioso:` link of a kind that doesn't exist. | | `Malformed folioso: parameter "PAIR"; expected name=value.` | A parameter with no `=`. | | `A folioso:page link takes an id and an optional slug, and nothing else.` | A page link with no `id`, or another parameter. | | `A folioso:mention link takes an id and optional char, taggedBy and label, and nothing else.` | A mention with no `id`, or another parameter. | | `A folioso:mention label must start with its suggestion character (@).` | Mention text that doesn't start with `@`, or with its `char`. | | `A folioso:span link takes one parameter: marks.` | A span with no `marks`, or another parameter. | | `folioso:span marks must be among: bold, italic, strike, underline.` | A mark that can't go in a span, such as `code`. | | `A folioso:attrs carrier must have an empty label: [](folioso:attrs?…).` | Text inside a carrier's brackets. | | `A folioso:attrs carrier must be the first thing in its paragraph, heading or table cell.` | A carrier after other text in its paragraph, heading or cell. | | `A folioso:attrs carrier must start a paragraph, a heading or a line holding one image.` | A carrier on a line that splits into several blocks. | | `A folioso:attrs carrier can't set "NAME" on TYPE.` | An attribute the block doesn't have, or an image's `src` or a heading's `level`. `TYPE` is the node type, such as `paragraph` or `tableCell`. | | `textAlign must be one of: left, center, right.` | Any other alignment. | | `[](folioso:empty) must be the whole document.` | The empty-document carrier alongside other content. | | ``A ```folioso block must hold one TipTap node as a JSON object with a string "type".`` | A JSON block that doesn't parse, or isn't a single node. | For example: ```markdown Ask [Ada](folioso:mention?id=5) to review. ``` ```json { "message": "A folioso:mention label must start with its suggestion character (@).", "errors": { "markdown": ["A folioso:mention label must start with its suggestion character (@)."] } } ``` A carrier that isn't the first thing in its paragraph: ```markdown Some text [](folioso:attrs?textAlign=center) ``` ```json { "message": "A folioso:attrs carrier must be the first thing in its paragraph, heading or table cell.", "errors": { "markdown": ["A folioso:attrs carrier must be the first thing in its paragraph, heading or table cell."] } } ``` --- # TipTap JSON reference A page's body is stored as a [TipTap](https://tiptap.dev) JSON document, the format the editor itself saves. This page describes every node, mark and attribute the editor uses, with an example of each and the Markdown Folioso writes for it. You get this format from `content` on a page, a recycle-bin page or a snapshot, and you can send it as `content` when you create or update a page. To work in Markdown instead, see [Content formats](/api/content-formats). ## The document The body is one `doc` node. Its `content` is an array of the page's top-level blocks: ```json { "type": "doc", "content": [ { "type": "heading", "attrs": { "level": 2 }, "content": [{ "type": "text", "text": "Install" }] }, { "type": "paragraph", "content": [ { "type": "text", "text": "Run " }, { "type": "text", "text": "composer install", "marks": [{ "type": "code" }] }, { "type": "text", "text": " first." } ] } ] } ``` ```markdown ## Install Run `composer install` first. ``` A page with no body has `content` of `null`, not a document. A document with an empty `content` array is a page with no blocks. ## Nodes Every node is a JSON object: | Key | Present on | Description | | --- | --- | --- | | `type` | Every node | The node's type, such as `paragraph`. | | `attrs` | Nodes with attributes | An object of attribute values. An attribute left out takes its default, so `{"type":"heading","attrs":{"level":2}}` has `textAlign` `null`. | | `content` | Nodes that hold other nodes | An array of child nodes. Left out, it's the same as `[]`. | | `text` | `text` nodes | The text. | | `marks` | `text`, `mention` and `pageLink` nodes | An array of marks, such as bold or a link. | Folioso keeps whatever JSON you send in `content`, without checking it against the rules below. The editor does apply them when it opens the page, so a node it doesn't know, or one in a place it can't go, may be dropped or moved when the page is next edited. Follow the rules on this page, and when in doubt, make the content in the editor and read it back with `GET /pages/{slug}`. ### What can go where | Node | Kind | Content | | --- | --- | --- | | `doc` | The document | Blocks and alert boxes, at least one. The editor opens a `doc` with `"content": []` as one empty paragraph. | | `paragraph`, `heading` | Block | Inline nodes. | | `blockquote` | Block | One or more blocks. | | `bulletList`, `orderedList` | Block | One or more `listItem`. | | `listItem` | Inside a list | A `paragraph`, then any blocks. | | `taskList` | Block | One or more `taskItem`. | | `taskItem` | Inside a task list | A `paragraph`, then any blocks. | | `codeBlock` | Block | `text` nodes without marks. | | `mermaid` | Block | Nothing. The source is an attribute. | | `horizontalRule`, `image` | Block | Nothing. | | `table` | Block | One or more `tableRow`. | | `tableRow` | Inside a table | `tableHeader` and `tableCell` nodes. | | `tableHeader`, `tableCell` | Inside a row | One or more blocks. | | `alertBox` | Top level only | An `alertTitle`, then an `alertBody`. | | `alertTitle` | Inside an alert box | Inline nodes. | | `alertBody` | Inside an alert box | One or more `paragraph`. | | `text`, `hardBreak`, `mention`, `pageLink` | Inline | Nothing. | "Blocks" means every block node except `alertBox`: an alert box can only sit at the top level of the page. ## Block nodes ### paragraph A paragraph of text. | Attribute | Type | Default | Description | | --- | --- | --- | --- | | `textAlign` | string or null | `null` | `"center"` or `"right"`. `null` is left-aligned. | ```json { "type": "paragraph", "attrs": { "textAlign": "center" }, "content": [{ "type": "text", "text": "Centred text." }] } ``` ```markdown [](folioso:attrs?textAlign=center)Centred text. ``` An empty paragraph, `{"type":"paragraph"}`, is an empty line on the page. ### heading A heading. | Attribute | Type | Default | Description | | --- | --- | --- | --- | | `level` | integer | `1` | 1 to 6. | | `textAlign` | string or null | `null` | As for [paragraph](#paragraph). | ```json { "type": "heading", "attrs": { "level": 3 }, "content": [{ "type": "text", "text": "Rollback" }] } ``` ```markdown ### Rollback ``` ### blockquote A quote. It has no attributes and holds blocks. ```json { "type": "blockquote", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Measure twice." }] }] } ``` ```markdown > Measure twice. ``` ### bulletList and listItem A bulleted list. Neither node has attributes. Each `listItem` starts with a `paragraph` and can hold further blocks, including nested lists. ```json { "type": "bulletList", "content": [ { "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Redis" }] }] }, { "type": "listItem", "content": [ { "type": "paragraph", "content": [{ "type": "text", "text": "Postgres" }] }, { "type": "bulletList", "content": [{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Version 18" }] }] }] } ] } ] } ``` ```markdown - Redis - Postgres - Version 18 ``` ### orderedList A numbered list of `listItem` nodes. | Attribute | Type | Default | Description | | --- | --- | --- | --- | | `start` | integer | `1` | The first item's number. | | `type` | string or null | `null` | The numbering style, as HTML's `type`: `"a"`, `"A"`, `"i"` or `"I"`. `null` numbers with digits. | ```json { "type": "orderedList", "attrs": { "start": 1 }, "content": [ { "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Build" }] }] }, { "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Ship" }] }] } ] } ``` ```markdown 1. Build 2. Ship ``` Markdown has no syntax for `type`, so a list with one is written as a [JSON block](/api/markdown-extensions#json-blocks). ### taskList and taskItem A checklist. Each `taskItem` starts with a `paragraph` and can hold further blocks, including nested task lists. | Attribute | On | Type | Default | Description | | --- | --- | --- | --- | --- | | `checked` | `taskItem` | boolean | `false` | Whether the box is ticked. | ```json { "type": "taskList", "content": [ { "type": "taskItem", "attrs": { "checked": true }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Tests pass" }] }] }, { "type": "taskItem", "attrs": { "checked": false }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Changelog" }] }] } ] } ``` ```markdown - [x] Tests pass - [ ] Changelog ``` ### codeBlock A block of code, holding plain `text` nodes with no marks. Line breaks are `\n` characters in the text. | Attribute | Type | Default | Description | | --- | --- | --- | --- | | `language` | string or null | Set by each reader | The language slug, such as `"php"` or `"bash"`. `null` is plain text. | The editor's language menu uses `typescript`, `javascript`, `php`, `python`, `go`, `rust`, `bash`, `sql`, `html`, `css`, `yaml`, `json` and `dockerfile`, with `null` for **Plain text**. Any other string is kept and shown by name. See [Code blocks](/editor/code-blocks#choose-the-language). `language` has no fixed default. A block with no `language` key at all is shown in each reader's own preferred language, which is why the editor always saves `language` explicitly. Send it explicitly too. ```json { "type": "codeBlock", "attrs": { "language": "php" }, "content": [{ "type": "text", "text": "echo 'Hello';\necho 'World';" }] } ``` ````markdown ```php echo 'Hello'; echo 'World'; ``` ```` An empty code block has no `content`. ### mermaid A diagram, drawn from its source. The type is named `mermaid` for both engines. | Attribute | Type | Default | Description | | --- | --- | --- | --- | | `code` | string | `""` | The diagram's source. | | `engine` | string | `"mermaid"` | `"mermaid"` or `"plantuml"`. PlantUML source includes its `@startuml` and `@enduml` lines. | ```json { "type": "mermaid", "attrs": { "code": "@startuml\nAlice -> Bob: Hello\n@enduml", "engine": "plantuml" } } ``` ````markdown ```plantuml @startuml Alice -> Bob: Hello @enduml ``` ```` See [Diagrams](/editor/diagrams). ### horizontalRule A horizontal line. No attributes or content. ```json { "type": "horizontalRule" } ``` ```markdown --- ``` ### image An image, always a block of its own. | Attribute | Type | Default | Description | | --- | --- | --- | --- | | `src` | string | `null` | The image's address. | | `alt` | string or null | `null` | The alt text, shown as the caption. | | `title` | string or null | `null` | The image's title. | | `width` | integer or null | `null` | The width in pixels, set by the resize handles. `null` is the image's natural width, up to the page width. | | `height` | integer or null | `null` | The height in pixels. The editor leaves it `null` and keeps the image's proportions. | | `textAlign` | string or null | `null` | `"center"` or `"right"` places the image. | ```json { "type": "image", "attrs": { "src": "https://folioso.dev/storage/images/0b9f4c5e-8d7a-4f1e-9c2b-3a4d5e6f7a8b.webp", "alt": "Request flow", "title": null, "width": 640, "height": null, "textAlign": null } } ``` ```markdown [](folioso:attrs?width=640)![Request flow]() ``` To use an image you upload through the API, put the `url` the upload returns in `src`, exactly as returned. A page write that names it keeps the upload. See [Images](/api/images#use-the-image-on-a-page). An image whose address no longer works shows **Image unavailable** in the editor. ### table, tableRow, tableHeader and tableCell A table is made of rows, and each row of header and ordinary cells. `table` and `tableRow` have no attributes. A new table in the editor has a header row, and **Toggle header row** and **Toggle header column** change which cells are `tableHeader`. | Attribute | On | Type | Default | Description | | --- | --- | --- | --- | --- | | `colspan` | cells | integer | `1` | How many columns the cell spans. | | `rowspan` | cells | integer | `1` | How many rows the cell spans. | | `colwidth` | cells | array or null | `null` | The column widths in pixels, one per column the cell spans, such as `[240]`. `null` lets the column size itself. | | `align` | cells | string or null | `null` | A cell alignment the editor doesn't set. Align a cell's paragraph with its `textAlign` instead. | Cells hold blocks, so a cell's text is a `paragraph`: ```json { "type": "table", "content": [ { "type": "tableRow", "content": [ { "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": null }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Queue" }] }] }, { "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": null }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Latency" }] }] } ] }, { "type": "tableRow", "content": [ { "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": null }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "mail" }] }] }, { "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": null }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "seconds" }] }] } ] } ] } ``` ```markdown | Queue | Latency | | --- | --- | | mail | seconds | ``` When cells span, a row has fewer cells. A table with spans, with cells holding more than one paragraph or other blocks, with no header row, or with header cells outside the first row has no pipe-table form, so it's written as a [JSON block](/api/markdown-extensions#json-blocks): ```json { "type": "table", "content": [ { "type": "tableRow", "content": [{ "type": "tableHeader", "attrs": { "colspan": 2 }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Merged" }] }] }] }, { "type": "tableRow", "content": [ { "type": "tableCell", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "a" }] }] }, { "type": "tableCell", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "b" }] }] } ] } ] } ``` ````markdown ```folioso {"type":"table","content":[{"type":"tableRow","content":[{"type":"tableHeader","attrs":{"colspan":2},"content":[{"type":"paragraph","content":[{"type":"text","text":"Merged"}]}]}]},{"type":"tableRow","content":[{"type":"tableCell","content":[{"type":"paragraph","content":[{"type":"text","text":"a"}]}]},{"type":"tableCell","content":[{"type":"paragraph","content":[{"type":"text","text":"b"}]}]}]}]} ``` ```` ### alertBox, alertTitle and alertBody An [alert box](/editor/alerts): a titled, coloured panel. It's made of exactly one `alertTitle`, a line of inline content that can carry marks, then one `alertBody` of paragraphs. Neither part has attributes. Alert boxes can only be top-level blocks, and their paragraphs can't be aligned. | Attribute | On | Type | Default | Description | | --- | --- | --- | --- | --- | | `type` | `alertBox` | string | `"info"` | `"info"`, `"success"`, `"warning"` or `"error"`. | ```json { "type": "alertBox", "attrs": { "type": "success" }, "content": [ { "type": "alertTitle", "content": [{ "type": "text", "text": "Deployed" }] }, { "type": "alertBody", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Version 4.2 is live." }] }] } ] } ``` ```markdown > [!TIP] Deployed > > Version 4.2 is live. ``` An empty title is `{"type":"alertTitle"}`. The body always holds at least one paragraph, which can be empty. | `type` | Shown as | Written in Markdown as | | --- | --- | --- | | `info` | Info | `[!NOTE]` | | `success` | Success | `[!TIP]` | | `warning` | Warning | `[!WARNING]` | | `error` | Error | `[!CAUTION]` | ## Inline nodes Inline nodes sit inside paragraphs, headings and alert titles. ### text A run of text. `text` must be a non-empty string: the editor refuses a document holding an empty text node. Neighbouring text with the same marks can be one node or several; both are the same. ```json { "type": "text", "text": "Hello" } ``` ### hardBreak A line break within a block. No attributes. ```json { "type": "paragraph", "content": [{ "type": "text", "text": "Line one" }, { "type": "hardBreak" }, { "type": "text", "text": "Line two" }] } ``` ```markdown Line one\ Line two ``` ### mention A mention of a person, shown as `@` and their name. Saving a page with a new mention notifies the person, following the rules in [Page links and mentions](/editor/links-and-mentions#who-you-can-mention). | Attribute | Type | Default | Description | | --- | --- | --- | --- | | `id` | number or string | `null` | The person's user id. The editor stores it as a number. | | `label` | string or null | `null` | The name shown. | | `mentionSuggestionChar` | string | `"@"` | The character the mention is shown with. | | `taggedBy` | number or null | `null` | The user id of the person who added the mention. The editor sets it. | ```json { "type": "paragraph", "content": [ { "type": "text", "text": "Thanks " }, { "type": "mention", "attrs": { "id": 5, "label": "Ada Lovelace", "mentionSuggestionChar": "@", "taggedBy": 7 } }, { "type": "text", "text": "!" } ] } ``` ```markdown Thanks [@Ada Lovelace](folioso:mention?id=5&taggedBy=7)\! ``` Mentions aren't included in search. ### pageLink A link to another Folioso page, shown with the target page's current title. Pages that link to a page are listed in its **Linked from**. | Attribute | Type | Default | Description | | --- | --- | --- | --- | | `id` | number | `null` | The target page's numeric id. | | `slug` | string | `null` | The target page's slug, which the editor uses to open it. | ```json { "type": "paragraph", "content": [ { "type": "text", "text": "See " }, { "type": "pageLink", "attrs": { "id": 42, "slug": "9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b" } }, { "type": "text", "text": "." } ] } ``` ```markdown See [Deploy runbook](folioso:page?id=42&slug=9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b). ``` - The title isn't stored. Each reader sees the title of a page they can open, and **Restricted page** for one they can't, so a page link never reveals a title to someone who can't see it. The Markdown label works the same way. - Links to a page that doesn't exist are kept in the content but don't count in **Linked from**. - Page links aren't included in search. ## Marks Marks style the text they're on. A node's `marks` array lists each mark once, and Folioso keeps them in this order: `link`, `bold`, `code`, `italic`, `strike`, `underline`. ```json { "type": "paragraph", "content": [ { "type": "text", "text": "bold", "marks": [{ "type": "bold" }] }, { "type": "text", "text": " " }, { "type": "text", "text": "italic", "marks": [{ "type": "italic" }] }, { "type": "text", "text": " " }, { "type": "text", "text": "struck", "marks": [{ "type": "strike" }] }, { "type": "text", "text": " " }, { "type": "text", "text": "underlined", "marks": [{ "type": "underline" }] }, { "type": "text", "text": " " }, { "type": "text", "text": "code", "marks": [{ "type": "code" }] } ] } ``` ```markdown **bold** *italic* ~~struck~~ [underlined](folioso:span?marks=underline) `code` ``` | Mark | Attributes | Description | | --- | --- | --- | | `bold` | None | Bold. | | `italic` | None | Italic. | | `strike` | None | Strikethrough. | | `underline` | None | Underline. | | `code` | None | Inline code. It can't be combined with any other mark. | | `link` | See below | A link to an address. | Marks combine, so `[{"type":"bold"},{"type":"italic"}]` is bold italic, written `***both***` in Markdown. ### link A link on a run of text. | Attribute | Type | Default | Description | | --- | --- | --- | --- | | `href` | string | `null` | The address. | | `target` | string or null | `"_blank"` | Where the link opens. The editor always uses `"_blank"`, a new tab. | | `rel` | string or null | `"noopener noreferrer"` | The link relationship. The editor always uses `"noopener noreferrer"`. | | `class` | string or null | `null` | Not used by the editor. | | `title` | string or null | `null` | The link's title. | ```json { "type": "paragraph", "content": [ { "type": "text", "text": "Read the " }, { "type": "text", "text": "guide", "marks": [ { "type": "link", "attrs": { "href": "https://example.com/guide", "target": "_blank", "rel": "noopener noreferrer", "class": null, "title": null } }, { "type": "bold" } ] }, { "type": "text", "text": "." } ] } ``` ```markdown Read the [**guide**](). ``` `href` is stored as given in JSON, whatever its scheme. A link with `target`, `rel` or `class` other than the defaults has no Markdown form and is written as a [JSON block](/api/markdown-extensions#json-blocks). To link to another Folioso page, use a [pageLink](#pagelink) rather than a link to its address. --- # Images Upload an image through the API, then use it on a page you create or update. An upload doesn't need a page, so you can upload first and create the page that shows it afterwards. All examples use `https://acme.folioso.dev/api/v1`; use your organisation's address instead. ## Upload an image ```http POST /images ``` Needs the `write` ability, and a plan that includes image uploads. See the [pricing page](https://folioso.dev/pricing). Send the image as `multipart/form-data`, not JSON: | Field | Rules | Description | | --- | --- | --- | | `image` | Required. A PNG, JPEG, GIF, WebP or AVIF file. | The image. The largest file your plan allows is the same as in the app. | | `alt` | Optional. Text, up to 500 characters. | The image's alt text. | ```bash curl https://acme.folioso.dev/api/v1/images \ -X POST \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" \ -F "image=@request-flow.png" \ -F "alt=Request flow" ``` The response is `201 Created`: ```json { "data": { "id": 318, "url": "https://folioso.dev/storage/images/0b9f4c5e-8d7a-4f1e-9c2b-3a4d5e6f7a8b.webp", "width": 1280, "height": 720, "alt": "Request flow", "expiresAt": "2026-10-20T11:15:02+00:00" } } ``` | Field | Type | Description | | --- | --- | --- | | `id` | integer | The upload's identifier. | | `url` | string | The stored image's address. Use it, exactly as returned, to put the image on a page. Its host can differ from your organisation's address. | | `width`, `height` | integer | The stored image's size in pixels. | | `alt` | string or null | The alt text you sent. | | `expiresAt` | string | The time by which a page write must use the image. See [How long an upload is kept](#how-long-an-upload-is-kept). | Uploads are processed exactly as in the app: the file's contents are checked, it's converted to WebP, and animated images are stored as a still. See [Images](/editor/images#formats-and-limits). Uploading the same file twice makes two separate uploads. An upload counts towards the organisation's image storage from the moment it's made, while it's still held, so a full organisation refuses the upload itself rather than the page write that uses it. ## Use the image on a page Put the upload's `url` in an image on a page you create or update with `POST /pages` or `PATCH /pages/{slug}`. In TipTap JSON, it's the image's `src`: ```json { "type": "image", "attrs": { "src": "https://folioso.dev/storage/images/0b9f4c5e-8d7a-4f1e-9c2b-3a4d5e6f7a8b.webp", "alt": "Request flow" } } ``` ```markdown ![Request flow]() ``` In Markdown, it's the image's address: ```markdown ![Request flow](https://folioso.dev/storage/images/0b9f4c5e-8d7a-4f1e-9c2b-3a4d5e6f7a8b.webp) ``` ```json { "type": "doc", "content": [ { "type": "image", "attrs": { "src": "https://folioso.dev/storage/images/0b9f4c5e-8d7a-4f1e-9c2b-3a4d5e6f7a8b.webp", "alt": "Request flow", "title": null } } ] } ``` The alt text on the page is whatever the image node says. The `alt` you sent with the upload isn't copied onto the page. To set the image's width or alignment, see [Block attributes](/api/markdown-extensions#image-size-and-alt-text). When a page write's body names an upload, the upload becomes one of that page's images, exactly like an image added in the editor. It's kept for as long as a page or snapshot uses it, and it counts towards the organisation's image storage. See [Uploads](/organisation/uploads#how-storage-is-counted). - The upload must be named in the **body** you send, as `content` or `markdown`. A `PATCH` that only renames the page doesn't use it. - Only an upload made with a token for the **same organisation** as the page can be used. Naming another organisation's upload doesn't move it. - One upload can be used on several pages. It belongs to the first page whose write names it. - A write refused with `409 Conflict` still makes the upload the page's image, although the page's content doesn't change. Write the page again to use it. If no page ever does, it's deleted like any image that's no longer used. Every other refusal (`403`, `404`, `422`) comes before the upload is used, so it stays held as before. ## How long an upload is kept An upload no page uses is held until its `expiresAt`, which is one hour after the upload by default. Once that time has passed, a clean-up that runs every 10 minutes deals with it: - **Still unused:** it's deleted. - **Used by a page in your organisation**, for example one written with a different token: it becomes that page's image, as if the page had named it. - **Used only elsewhere**, such as in another organisation's page or in a snapshot: it's kept for as long as that content uses it, and isn't counted towards your storage. So write the page before `expiresAt`. An upload can survive for up to about 10 minutes after it, but don't rely on that. ## Errors | Status | Message | Why | | --- | --- | --- | | `403` | `Invalid ability provided.` | The token is read-only. | | `403` | `Your plan does not include this feature.` | The organisation's plan doesn't include image uploads. | | `404` | | Image uploads are switched off on this Folioso installation. | | `413` | `That image has too many pixels.` | The image is over 25 megapixels. | | `413` | `That image is too large.` | The image is still over 25 MB once converted to WebP. | | `422` | `Images must be under N MB on your plan.` | The file is over your plan's largest image. It's also under `errors.image`. | | `422` | `This organisation doesn't have room for this image within its … of image storage. …` | The organisation's image storage is full. The body also has an `upgrade` object naming the limit. | | `422` | `That image could not be read.` | The file is damaged or isn't really an image. | | `422` | | `image` is missing, or its contents aren't a PNG, JPEG, GIF, WebP or AVIF image whatever its name (under `errors.image`), or `alt` is too long. See `errors`. | | `429` | `Too Many Attempts.` | Your account has uploaded more than 50 images in the last hour. The limit is shared with every image you upload in the app too, including avatars and logos. See [Rate limits](/api/authentication#rate-limits). | --- # Spaces, groups and tags Read the spaces you can reach, and the groups and tags inside them, so you know where to file pages and how to filter them. These endpoints are read-only and need the `read` ability. All three return every result at once, without pagination, wrapped in `data`. Spaces and groups are created and changed in the app. Tags are too, and the API also creates a tag when you name one a space doesn't have yet on a page. See [Tags](/api/pages#tags). ## List spaces ```http GET /spaces ``` Returns the spaces you can open in the organisation, sorted by name. That includes open spaces, closed and private spaces you've been given a role in, and your own personal space. Spaces you can't open aren't listed. ```bash curl https://acme.folioso.dev/api/v1/spaces \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" ``` ```json { "data": [ { "slug": "engineering", "name": "Engineering", "accessType": "closed", "isPersonal": false, "isDefault": false }, { "slug": "general", "name": "General", "accessType": "open", "isPersonal": false, "isDefault": true }, { "slug": "personal-42", "name": "My space", "accessType": "private", "isPersonal": true, "isDefault": false } ] } ``` | Field | Type | Description | | --- | --- | --- | | `slug` | string | The space's identifier. Use it as `space` when creating pages and filtering lists. | | `name` | string | The space's name. | | `accessType` | string | `open`, `closed` or `private`. See [Spaces](/spaces/overview). | | `isPersonal` | boolean | `true` for your personal space ("My space"). | | `isDefault` | boolean | `true` for the organisation's default space. | ## List groups ```http GET /groups ``` Returns the groups in the spaces you can open, in the order they appear in the sidebar. | Parameter | Description | | --- | --- | | `space` | Only groups in the space with this slug. An unknown slug gives an empty list. | ```bash curl "https://acme.folioso.dev/api/v1/groups?space=engineering" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" ``` ```json { "data": [ { "id": 12, "name": "Runbooks", "description": "How we operate production.", "colour": "#3b82f6", "space": "engineering" }, { "id": 14, "name": "Architecture", "description": null, "colour": null, "space": "engineering" } ] } ``` | Field | Type | Description | | --- | --- | --- | | `id` | integer | The group's identifier. Use it as `group_id` when creating a page and as `group` when filtering pages. | | `name` | string | The group's name. | | `description` | string or null | The group's description. | | `colour` | string or null | The group's colour. | | `space` | string | The slug of the space the group belongs to. | See [Groups](/spaces/groups). ## List tags ```http GET /tags ``` Returns the tags in the spaces you can open, sorted by name. Tags belong to a space, so two spaces can each have a tag with the same name; they're different tags with different `id`s. | Parameter | Description | | --- | --- | | `space` | Only tags in the space with this slug. An unknown slug gives an empty list. | ```bash curl "https://acme.folioso.dev/api/v1/tags?space=engineering" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" ``` ```json { "data": [ { "id": 31, "name": "php", "colour": "#777bb3", "isDefault": true }, { "id": 40, "name": "release", "colour": "#22c55e", "isDefault": false } ] } ``` | Field | Type | Description | | --- | --- | --- | | `id` | integer | The tag's identifier. Use it as `tag` when filtering pages. | | `name` | string | The tag's name. | | `colour` | string or null | The tag's colour. | | `isDefault` | boolean | `true` for the language tags every space starts with. | To set a page's tags, send their names as `tags` when you create or update it. Names a space doesn't have yet are created there. See [Tags](/api/pages#tags). Renaming, recolouring and deleting tags is done in the app. See [Tags](/editor/tags). ## Putting it together To file a new page in a group with tags, look up the space and group first, then create the page: ```bash curl "https://acme.folioso.dev/api/v1/groups?space=engineering" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" curl https://acme.folioso.dev/api/v1/pages \ -X POST \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "title": "Restart the queue workers", "space": "engineering", "group_id": 12, "tags": ["runbook"] }' ``` To list every page with a tag, use its `id`: ```bash curl "https://acme.folioso.dev/api/v1/pages?tag=40" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" ``` See [Pages](/api/pages). --- # Recycle bin and snapshots Read the pages in your recycle bin, and list, read and take snapshots of a page. These endpoints read what the app's [recycle bin](/spaces/recycle-bin) and [version history](/editor/version-history) show. Restoring a page from the bin, deleting it forever and restoring a snapshot are done in the app. All examples use `https://acme.folioso.dev/api/v1`; use your organisation's address instead. ## List your recycle bin ```http GET /recycle-bin ``` Needs the `read` ability. Lists the same pages as your own **Recycle bin** in the app's sidebar: - every page you deleted, in any space of the organisation, including My space; and - every page deleted by anyone in the spaces you administer. Organisation admins administer every shared space. It never lists pages from another member's My space. Pages are listed most recently deleted first. | Parameter | Description | | --- | --- | | `space` | Only pages in the space with this slug. An unknown slug gives an empty list. | | `per_page` | Results per page, from 1 to 100. Default 25. A value outside that range is treated as the nearest limit, not refused. | | `page` | Which page of results to return. Default 1. | ```bash curl "https://acme.folioso.dev/api/v1/recycle-bin?space=engineering" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" ``` ```json { "data": [ { "id": 1207, "slug": "1f0e2d3c-5b6a-4978-8a9b-0c1d2e3f4a5b", "title": "Old on-call rota", "excerpt": null, "visibility": "space", "space": { "slug": "engineering", "name": "Engineering" }, "group": null, "url": null, "createdAt": "2026-08-02T10:00:00+00:00", "updatedAt": "2026-10-01T16:20:41+00:00", "deletedAt": "2026-10-18T09:30:00+00:00", "purgeAt": "2026-11-17T09:30:00+00:00", "deletedBy": { "name": "Ada Lovelace" } } ], "links": { "first": "https://acme.folioso.dev/api/v1/recycle-bin?space=engineering&page=1", "last": "https://acme.folioso.dev/api/v1/recycle-bin?space=engineering&page=1", "prev": null, "next": null }, "meta": { "current_page": 1, "from": 1, "last_page": 1, "path": "https://acme.folioso.dev/api/v1/recycle-bin", "per_page": 25, "to": 1, "total": 1 } } ``` Each item has the fields of a [page object](/api/pages#the-page-object), without `tags` and content, plus: | Field | Type | Description | | --- | --- | --- | | `url` | null | Always `null`. A page in the bin can't be opened in the editor until it's restored. | | `excerpt` | null | Always `null`. The bin lists pages you may not be allowed to read, such as a colleague's private page in a space you administer, so it never previews their text. Read the page itself for its body. | | `deletedAt` | string | When the page was deleted. | | `purgeAt` | string | When the page becomes due for permanent deletion. Pages are permanently deleted by a nightly clean-up, so it goes at the first run after this time. See [Retention](/spaces/recycle-bin#retention). | | `deletedBy` | object or null | The `name` of the person who deleted it, or `null` if their account no longer exists. | Pagination works as for [List pages](/api/pages#pagination). ## Get a page in the bin ```http GET /recycle-bin/{slug} ``` Needs the `read` ability. Returns one page from your recycle bin with its `tags`, `content` and `contentEpoch`, in the same shape as a bin list item. Add `?format=markdown` to get its body as `markdown` instead. See [Content formats](/api/content-formats). | Status | When | | --- | --- | | `404` | The page isn't in your recycle bin: it doesn't exist, belongs to another organisation, hasn't been deleted, or is in a bin you can't see. | | `403` | The page is in your bin, but you can't view it. | | `422` | `format` isn't `json` or `markdown`, or you asked for Markdown and the stored content can't be written as Markdown without loss. See [When Markdown isn't available](/api/content-formats#when-markdown-isnt-available). | > [!NOTE] > A space admin's bin lists every page deleted in the space, including other people's **private** pages, by title, as the app does. Their content is still private: their bin item carries no `excerpt`, and getting one of those pages answers `403`. ## List a page's snapshots ```http GET /pages/{slug}/snapshots ``` Needs the `read` ability, a plan that includes version history, and the editor or admin role in the page's space, or authorship of the page. On a private page, only its author. People who can open a page only because it was shared with them can't see its history, as in the app. See [Who can see snapshots](/editor/version-history#who-can-see-snapshots). Snapshots are listed newest first, without their content. Snapshots from before the page's audience last changed are hidden, as in the app. ```bash curl https://acme.folioso.dev/api/v1/pages/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b/snapshots \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" ``` ```json { "data": [ { "id": 2048, "kind": "manual", "label": "Before the refactor", "title": "Deploying the API", "wordCount": 412, "createdAt": "2026-10-16T09:00:12+00:00", "createdBy": { "name": "Ada Lovelace" } }, { "id": 1990, "kind": "auto", "label": null, "title": "Deploying the API", "wordCount": 398, "createdAt": "2026-10-15T03:15:40+00:00", "createdBy": null } ], "links": { "first": null, "last": null, "prev": null, "next": "https://acme.folioso.dev/api/v1/pages/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b/snapshots?cursor=eyJpZCI6MTk5MCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ" }, "meta": { "path": "https://acme.folioso.dev/api/v1/pages/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b/snapshots", "per_page": 50, "next_cursor": "eyJpZCI6MTk5MCwiX3BvaW50c1RvTmV4dEl0ZW1zIjp0cnVlfQ", "prev_cursor": null } } ``` | Field | Type | Description | | --- | --- | --- | | `id` | integer | The snapshot's identifier. | | `kind` | string | `auto` (a daily snapshot), `manual` or `pre_restore` (taken automatically before a restore). See [Kinds of snapshot](/editor/version-history#kinds-of-snapshot). | | `label` | string or null | The label given to a manual snapshot. | | `title` | string | The page's title when the snapshot was taken. | | `wordCount` | integer | The number of words in the snapshot. | | `createdAt` | string | When the snapshot was taken. | | `createdBy` | object or null | The `name` of the person who took it, or `null` for a daily snapshot or a person whose account no longer exists. | The list returns 50 snapshots at a time. Follow `links.next`, or pass `meta.next_cursor` as `cursor`, until it's `null`. ## Get a snapshot ```http GET /pages/{slug}/snapshots/{id} ``` Needs the same as listing them. Returns one snapshot with its body as `content`, or as `markdown` with `?format=markdown`. Snapshots have no `contentEpoch`. ```bash curl "https://acme.folioso.dev/api/v1/pages/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b/snapshots/2048?format=markdown" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" ``` ```json { "data": { "id": 2048, "kind": "manual", "label": "Before the refactor", "title": "Deploying the API", "wordCount": 412, "markdown": "## Install\n\nRun `composer install` first.", "createdAt": "2026-10-16T09:00:12+00:00", "createdBy": { "name": "Ada Lovelace" } } } ``` A snapshot of another page, or one hidden because the page's audience has changed since it was taken, gets `404`. ## Take a snapshot ```http POST /pages/{slug}/snapshots ``` Needs the `write` ability and the same plan and role as listing them. Takes a manual snapshot of the page as it was last saved, like **Take snapshot** in the app. | Field | Rules | Description | | --- | --- | --- | | `label` | Optional. Text, up to 80 characters, or `null`. | A label for the snapshot. Leading and trailing spaces are trimmed, and a blank label is no label. | ```bash curl https://acme.folioso.dev/api/v1/pages/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b/snapshots \ -X POST \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "label": "Before the refactor" }' ``` The response is `201 Created` with the new snapshot, without its content, in the shape above. Its `kind` is `manual` and `createdBy` is you. You can take up to 10 snapshots a minute. The allowance is shared with taking and restoring snapshots in the app. ## Errors | Status | When | | --- | --- | | `403` | `Your plan does not include this feature.`: the plan doesn't include version history. | | `403` | `This action is unauthorized.`: you don't have the editor or admin role in the page's space and didn't write the page. | | `403` | `Invalid ability provided.`: a read-only token tried to take a snapshot. | | `404` | The page or snapshot doesn't exist, belongs to another organisation, or the snapshot is hidden. | | `422` | A `format` other than `json` or `markdown`, a `label` over 80 characters, or a request for a snapshot as Markdown when its content can't be written as Markdown without loss (see [When Markdown isn't available](/api/content-formats#when-markdown-isnt-available)). | | `429` | You've taken more than 10 snapshots in the last minute. | --- # API examples Four complete workflows: creating a page from Markdown you've written, editing a page as Markdown and writing it back, uploading an image and using it on a page, and moving, regrouping and tagging a page in one request. All examples use `https://acme.folioso.dev/api/v1`; use your organisation's address instead. Each one needs a token with the `read` and `write` abilities. See [Authentication and limits](/api/authentication). ## Create a page from Markdown Write the page as ordinary Markdown: ```markdown Release 4.2 ships on **Thursday**. ## Checklist - [x] Tag the release - [ ] Publish the notes > [!NOTE] Rollback > Redeploy 4.1 with `./deploy 4.1`. | Service | Owner | | --- | --- | | API | Platform | ``` Send it as `markdown`, JSON-encoded like any other string, with the page's title, space, group and tags: ```bash curl https://acme.folioso.dev/api/v1/pages \ -X POST \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "title": "Release 4.2", "space": "engineering", "group_id": 12, "tags": ["release"], "markdown": "Release 4.2 ships on **Thursday**.\n\n## Checklist\n\n- [x] Tag the release\n- [ ] Publish the notes\n\n> [!NOTE] Rollback\n> Redeploy 4.1 with `./deploy 4.1`.\n\n| Service | Owner |\n| --- | --- |\n| API | Platform |" }' ``` The response is `201 Created` with the new page. Its `content` is the Markdown converted to the editor's format: a paragraph, a heading, a task list, an info [alert box](/editor/alerts) titled "Rollback" and a table. The page looks exactly as if it had been typed in the editor: ```json { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "Release 4.2 ships on " }, { "type": "text", "text": "Thursday", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "." } ] }, { "type": "heading", "attrs": { "level": 2 }, "content": [{ "type": "text", "text": "Checklist" }] }, { "type": "taskList", "content": [ { "type": "taskItem", "attrs": { "checked": true }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Tag the release" }] }] }, { "type": "taskItem", "attrs": { "checked": false }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Publish the notes" }] }] } ] }, { "type": "alertBox", "attrs": { "type": "info" }, "content": [ { "type": "alertTitle", "content": [{ "type": "text", "text": "Rollback" }] }, { "type": "alertBody", "content": [ { "type": "paragraph", "content": [ { "type": "text", "text": "Redeploy 4.1 with " }, { "type": "text", "text": "./deploy 4.1", "marks": [{ "type": "code" }] }, { "type": "text", "text": "." } ] } ] } ] }, { "type": "table", "content": [ { "type": "tableRow", "content": [ { "type": "tableHeader", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Service" }] }] }, { "type": "tableHeader", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Owner" }] }] } ] }, { "type": "tableRow", "content": [ { "type": "tableCell", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "API" }] }] }, { "type": "tableCell", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Platform" }] }] } ] } ] } ] } ``` If `release` isn't a tag in the Engineering space yet, it's created. Add `?format=markdown` to the address to get the body back as Markdown instead. ## Edit a page as Markdown Say a page's stored content is this: ```json { "type": "doc", "content": [ { "type": "heading", "attrs": { "level": 2, "textAlign": null }, "content": [{ "type": "text", "text": "Deploy steps" }] }, { "type": "paragraph", "attrs": { "textAlign": null }, "content": [ { "type": "text", "text": "Ask " }, { "type": "mention", "attrs": { "id": 5, "label": "Ada Lovelace", "mentionSuggestionChar": "@", "taggedBy": 7 } }, { "type": "text", "text": " before you start, and follow " }, { "type": "pageLink", "attrs": { "id": 42, "slug": "9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b" } }, { "type": "text", "text": "." } ] }, { "type": "orderedList", "attrs": { "start": 1, "type": null }, "content": [ { "type": "listItem", "content": [{ "type": "paragraph", "attrs": { "textAlign": null }, "content": [{ "type": "text", "text": "Run the migrations" }] }] }, { "type": "listItem", "content": [{ "type": "paragraph", "attrs": { "textAlign": null }, "content": [{ "type": "text", "text": "Restart the workers" }] }] } ] }, { "type": "alertBox", "attrs": { "type": "warning" }, "content": [ { "type": "alertTitle", "content": [{ "type": "text", "text": "Never on a Friday" }] }, { "type": "alertBody", "content": [ { "type": "paragraph", "attrs": { "textAlign": null }, "content": [ { "type": "text", "text": "Deploys " }, { "type": "text", "text": "freeze", "marks": [{ "type": "underline" }] }, { "type": "text", "text": " at 15:00." } ] } ] } ] }, { "type": "codeBlock", "attrs": { "language": "bash" }, "content": [{ "type": "text", "text": "php artisan migrate --force" }] } ] } ``` ### 1. Read it as Markdown ```bash curl "https://acme.folioso.dev/api/v1/pages/1f0e2d3c-5b6a-4978-8a9b-0c1d2e3f4a5b?format=markdown" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" ``` The response's `markdown` holds the page, and `contentEpoch` its current epoch, say `3`. The mention, the page link and the underline are written with [Folioso extensions](/api/markdown-extensions), and the page link's label is the target's title as you see it: ````markdown ## Deploy steps Ask [@Ada Lovelace](folioso:mention?id=5&taggedBy=7) before you start, and follow [Deploy runbook](folioso:page?id=42&slug=9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b). 1. Run the migrations 2. Restart the workers > [!WARNING] Never on a Friday > > Deploys [freeze](folioso:span?marks=underline) at 15:00. ```bash php artisan migrate --force ``` ```` ### 2. Change it Edit the Markdown as text. Here the second step is reworded and a third added: ````markdown ## Deploy steps Ask [@Ada Lovelace](folioso:mention?id=5&taggedBy=7) before you start, and follow [Deploy runbook](folioso:page?id=42&slug=9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b). 1. Run the migrations 2. Restart the queue workers 3. Check Horizon > [!WARNING] Never on a Friday > > Deploys [freeze](folioso:span?marks=underline) at 15:00. ```bash php artisan migrate --force ``` ```` ### 3. Write it back Send the whole Markdown as `markdown`, with the epoch you read: ```bash curl https://acme.folioso.dev/api/v1/pages/1f0e2d3c-5b6a-4978-8a9b-0c1d2e3f4a5b \ -X PATCH \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ --data-binary @body.json ``` where `body.json` is: ```json { "markdown": "## Deploy steps\n\nAsk [@Ada Lovelace](folioso:mention?id=5&taggedBy=7) before you start, and follow [Deploy runbook](folioso:page?id=42&slug=9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b).\n\n1. Run the migrations\n2. Restart the queue workers\n3. Check Horizon\n\n> [!WARNING] Never on a Friday\n>\n> Deploys [freeze](folioso:span?marks=underline) at 15:00.\n\n```bash\nphp artisan migrate --force\n```", "epoch": 3 } ``` The page now has the three-step list: ```json { "type": "orderedList", "attrs": { "start": 1 }, "content": [ { "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Run the migrations" }] }] }, { "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Restart the queue workers" }] }] }, { "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Check Horizon" }] }] } ] } ``` Everything else is the same document as before: Ada is still mentioned, the page link still points at page 42, "freeze" is still underlined and the code block is still Bash. The only differences in the stored JSON are attributes at their defaults, such as `"textAlign": null`, which may be left out. See [What "the same document" means](/api/content-formats#what-the-same-document-means). The response has the new `contentEpoch`. Anyone with the page open is reloaded with your version. If the page was restored from a snapshot or replaced since you read it, you get `409 Conflict` instead and nothing changes: read it again and reapply your edit. ## Upload an image and use it ### 1. Upload the image ```bash curl https://acme.folioso.dev/api/v1/images \ -X POST \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" \ -F "image=@request-flow.png" ``` ```json { "data": { "id": 318, "url": "https://folioso.dev/storage/images/0b9f4c5e-8d7a-4f1e-9c2b-3a4d5e6f7a8b.webp", "width": 1280, "height": 720, "alt": null, "expiresAt": "2026-10-20T11:15:02+00:00" } } ``` ### 2. Use it in Markdown Use the `url` as the image's address, before `expiresAt`. A [block attributes](/api/markdown-extensions#image-size-and-alt-text) carrier sets its width and centres it: ```markdown How a request reaches the API: [](folioso:attrs?width=640&textAlign=center)![Request flow](https://folioso.dev/storage/images/0b9f4c5e-8d7a-4f1e-9c2b-3a4d5e6f7a8b.webp) ``` ```json { "type": "doc", "content": [ { "type": "paragraph", "content": [{ "type": "text", "text": "How a request reaches the API:" }] }, { "type": "image", "attrs": { "src": "https://folioso.dev/storage/images/0b9f4c5e-8d7a-4f1e-9c2b-3a4d5e6f7a8b.webp", "alt": "Request flow", "title": null, "width": 640, "textAlign": "center" } } ] } ``` Send it as `markdown` on `POST /pages` or `PATCH /pages/{slug}`: ```bash curl https://acme.folioso.dev/api/v1/pages \ -X POST \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "title": "Request flow", "space": "engineering", "markdown": "How a request reaches the API:\n\n[](folioso:attrs?width=640&textAlign=center)![Request flow](https://folioso.dev/storage/images/0b9f4c5e-8d7a-4f1e-9c2b-3a4d5e6f7a8b.webp)" }' ``` ### 3. Or use it in TipTap JSON The same image as `content` puts the `url` in `src`: ```bash curl https://acme.folioso.dev/api/v1/pages \ -X POST \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "title": "Request flow", "space": "engineering", "content": { "type": "doc", "content": [ { "type": "paragraph", "content": [{ "type": "text", "text": "How a request reaches the API:" }] }, { "type": "image", "attrs": { "src": "https://folioso.dev/storage/images/0b9f4c5e-8d7a-4f1e-9c2b-3a4d5e6f7a8b.webp", "alt": "Request flow", "width": 640, "textAlign": "center" } } ] } }' ``` Either way, the write keeps the upload as one of the page's images. An upload no page uses by `expiresAt` is deleted. See [Images](/api/images#how-long-an-upload-is-kept). ## Move, regroup and tag a page One `PATCH` can move a page to another space, put it in a group there, and set its tags: ```bash curl https://acme.folioso.dev/api/v1/pages/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b \ -X PATCH \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "space": "operations", "group_id": 27, "tags": ["runbook", "deploy"] }' ``` 1. Look up the target space's slug with `GET /spaces`, and the group's `id` with `GET /groups?space=operations`. See [Spaces, groups and tags](/api/spaces-groups-and-tags). 2. You need the editor or admin role in the page's current space, or to be its author, and the editor or admin role in the target space. 3. `group_id` must be a group in the **target** space. Leave it out to put the page outside any group there. 4. `tags` is the page's complete set of tags in the target space. `runbook` and `deploy` are matched to the Operations space's tags by exact name, and any that don't exist there are created. The response is the updated page. Its `content` and `contentEpoch` are left out here: ```json { "data": { "id": 42, "slug": "9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b", "title": "Deploying the API", "excerpt": null, "visibility": "space", "space": { "slug": "operations", "name": "Operations" }, "group": { "id": 27, "name": "Runbooks" }, "tags": [{ "id": 85, "name": "runbook" }, { "id": 88, "name": "deploy" }], "url": "https://acme.folioso.dev/operations/9b2f6c1e-4d1a-4b8e-9f0a-2c3d4e5f6a7b", "createdAt": "2026-10-01T14:03:11+00:00", "updatedAt": "2026-10-20T10:02:57+00:00" } } ``` The page's slug doesn't change, so links to it keep working, and the pages nested under it move too. `excerpt` is `null` until the moved page is next indexed for search, as after any change. Snapshots from before the move are hidden, because the page has a new audience. Body changes can go in the same request: the title and body are written first, then the page is moved, then tagged. See [Move, regroup and tag a page](/api/pages#move-regroup-and-tag-a-page). To regroup a page within its own space, send only `group_id`, or `"group_id": null` to take it out of its group. To change only its tags, send only `tags`. --- # Errors The API always answers in JSON, including errors and unknown addresses. Every error body has a `message`, and validation errors add an `errors` object naming each field. ## Status codes | Status | Meaning | What to do | | --- | --- | --- | | `200 OK` | The request worked. | | | `201 Created` | A page, image or snapshot was created. | | | `204 No Content` | A page was moved to the recycle bin. The body is empty. | | | `308 Permanent Redirect` | Your organisation's address has changed. | Update your base address. See [308: organisation has moved](#308-organisation-has-moved). | | `401 Unauthorized` | The token is missing, wrong, revoked or expired. | Check the `Authorization` header, or create a new token. | | `403 Forbidden` | The token is valid, but this request isn't allowed. | Read the `message`. See [403: not allowed](#403-not-allowed). | | `404 Not Found` | The page, space, group or organisation doesn't exist, isn't in this token's organisation, or the endpoint doesn't exist. | Check the slug, the id and the base address. | | `405 Method Not Allowed` | The endpoint exists but not with this method. | Check the method, for example `PATCH` rather than `PUT`. | | `409 Conflict` | The page was restored from a snapshot, or its content was replaced through the API, since you read it. | Read the page again and reapply your change. See [Pages](/api/pages#avoid-overwriting-a-restore). | | `413 Content Too Large` | An uploaded image has too many pixels, or is still too large once converted. | Upload a smaller image. See [Images](/api/images#errors). | | `422 Unprocessable Content` | The request body or a parameter failed validation (an uploaded file that isn't a supported image is one), Markdown holds a malformed Folioso extension, or you asked for a page as Markdown that can't be written as Markdown without loss. | Fix the fields listed in `errors`. See [Folioso Markdown extensions](/api/markdown-extensions#error-messages) and [Content formats](/api/content-formats#when-markdown-isnt-available). | | `429 Too Many Requests` | You've gone over a rate limit. | Wait for the number of seconds in `Retry-After`. See [Authentication and limits](/api/authentication#rate-limits). | | `500 Internal Server Error` | Something went wrong on Folioso's side. | Try again later. | ## Error bodies Most errors look like this: ```json { "message": "Unauthenticated." } ``` ### 403: not allowed A `403` means the token was accepted but this request isn't allowed. The `message` says why: | Message | Cause | | --- | --- | | `Invalid ability provided.` | A read-only token called a write endpoint. | | `Your plan does not include this feature.` | The organisation's plan doesn't include API access. | | `You are no longer a member of this organisation.` | You've left or been removed from the organisation. | | `Your account in this organisation is suspended. Contact an administrator.` | Your membership is suspended. | | `This account must enrol in two-factor authentication before its API tokens can be used.` | You need to set up two-factor authentication. | | `This action is unauthorized.` | Your role doesn't allow this, for example creating a page in a space where you're a viewer, deleting a page you didn't write, moving or tagging a page that's only shared with you, or reading a private page in your recycle bin. | ### 422: validation errors ```json { "message": "The title field is required. (and 1 more error)", "errors": { "title": ["The title field is required."], "space": ["The space field is required."] } } ``` Each key in `errors` is a field name, and each value lists what's wrong with it. ### 404: not found A `404` is the same whether something doesn't exist or belongs to another organisation, so a token can't be used to discover what exists elsewhere. A space you can't see, such as a private space you have no role in or a colleague's personal space, answers the same `404` as a space that doesn't exist. When you create or move a page, the `message` says which part wasn't found: ```json { "message": "No such group in that space." } ``` ### 308: organisation has moved When an organisation changes its address, Folioso redirects requests from the old address for 30 days. Requests made with a token from that organisation get a `308` with the new address in the `Location` header and in the body: ```json { "message": "This organisation now lives at https://acme-labs.folioso.dev. Update your base URL.", "movedTo": "https://acme-labs.folioso.dev/api/v1/pages?space=engineering" } ``` A `308` keeps the method and body, so a client that follows redirects repeats a `POST` or `PATCH` correctly. Not every client follows redirects automatically, so update your base address as soon as you see one. After 30 days, the old address returns `404`. ## Retrying safely - **`429` and `5xx`**: wait and retry. For `429`, honour `Retry-After`. - **`409`**: read the page again before retrying. Sending the same body again would undo the restore. - **`401`, `403`, `404` and `422`**: retrying won't help until you change the request or the token. - **`POST /pages`** isn't idempotent. If a create times out, list the space's pages before retrying, or you may create the page twice. - **`POST /images`** isn't idempotent either, but retrying is harmless: an upload no page uses is deleted after its `expiresAt`. See [Images](/api/images#how-long-an-upload-is-kept). --- # Frequently asked questions Short answers to the questions people ask most about accounts, plans and billing, sharing, security and your data, with links to the full details. ## Account ### Can I sign in with GitHub? Yes. You can sign in with an email and password or with GitHub. If your organisation's plan includes it, your organisation can also set up single sign-on through its identity provider, alongside the usual sign-in. See [Signing in](/account/signing-in) and [Single sign-on](/organisation/single-sign-on). ### Can I belong to more than one organisation? Yes. Every account comes with its own organisation, and you can join others by invitation. Each organisation has its own address, members and plan. See [Working in several organisations](/organisation/multiple-organisations). ### Can I undo deleting my account? Yes, for 30 days. Deleting your account starts a 30-day grace window, and Folioso emails you a link to reactivate it. After that the account is erased for good. See [Deleting your account](/account/deleting-your-account). ### What happens to my pages if I leave an organisation? They stay in the organisation. Leaving removes your access, not your work. Pages that only you could see, such as private pages and anything in your personal space, then appear to the organisation's admins under content left by former members, so they can hand them to someone else or delete them. See [Former members' content](/organisation/former-members-content). ## Plans and billing ### Is there a free plan? Yes. Every organisation starts on Free, and it's also the plan you return to if you stop paying. Its limits are lower than the paid plans'. See the [pricing page](https://folioso.dev/pricing) for what it includes, and [Plans](/billing/plans) for how plans work. ### How much do the paid plans cost? See the [pricing page](https://folioso.dev/pricing) for current prices. ### Who can change the plan? Organisation admins. Members don't see billing at all. See [Choosing a plan](/billing/choosing-a-plan). ### What's the difference between flat and per-seat plans? A flat plan charges one price for the organisation and includes a fixed number of seats. A per-seat plan charges for each member and may have a minimum number of seats. Plans also differ in their limits and features. See [Plans](/billing/plans#flat-and-per-seat-plans) and the [pricing page](https://folioso.dev/pricing). ### What happens if I add a member mid-month? On a per-seat plan, the new seat is charged only for the rest of the current billing period. Removing someone lowers your seat count at the next renewal. See [Seats](/billing/seats). ### When does a plan change take effect? Upgrades apply straight away and are charged pro rata. Downgrades and moves to Free wait until the end of the billing period, so you keep what you've paid for. See [Upgrading](/billing/upgrading) and [Downgrading](/billing/downgrading). ### If I move to Free, will I lose my pages? No. Nothing you've created is deleted. Free's limits and features apply, and members beyond the seats Free includes are suspended until you upgrade again. See [Moving to the Free plan](/billing/returning-to-free). ### How do I pay? There's no card payment in Folioso yet. Invoices are issued after each billing period closes and are payable offline. See [Invoices](/billing/invoices). ### What happens if I don't pay an invoice? Folioso sends reminders 1, 7 and 14 days after the due date. If it's still unpaid 21 days after the due date, the organisation moves to Free, and the invoice remains owed. See [Overdue payments](/billing/overdue-payments). ### Is real-time collaboration a paid feature? No. Live editing together works on every plan. See [Editing together](/editor/collaboration). ### Is the API available on every plan? No. API access is a plan feature, so it depends on your organisation's plan. See the [pricing page](https://folioso.dev/pricing) and [API overview](/api/overview). ## Sharing ### Can I share a page with someone outside my organisation? Yes. Turn on a public link, which anyone with the address can open, read-only or with edit access, and optionally protected by a password. You can also share a page directly with a person. See [Public links](/sharing/public-links) and [Sharing with people](/sharing/sharing-with-people). ### Will search engines find my public links? No. Folioso tells search engines not to index shared pages, though link previews in chat apps still work. See [Public links](/sharing/public-links). ### Why can't I share more pages? Your plan may limit how many pages you can have shared at once. Stop sharing one, or upgrade to a plan with a higher limit. See [Shared page limits](/sharing/sharing-with-people#shared-page-limits) and the [pricing page](https://folioso.dev/pricing). ## Security ### Do I have to use two-factor authentication? It's optional for most people, but required for admins and owners of an organisation that has more than one member. See [Two-factor authentication](/account/two-factor-authentication). ### Can my organisation's admins read my personal space? Not while you're a member. Your personal space ("My space") is private to you. No one else holds a role there, organisation admins included. If you leave the organisation, any pages left in it become visible to admins as former members' content. See [About spaces](/spaces/overview) and [Former members' content](/organisation/former-members-content). ### Can I see who did what in my organisation? If your plan includes the audit log, admins can view and export a log of security events, such as members joining and settings changing. See [Audit log](/organisation/audit-log). ## Your data ### Can I export everything? Yes. **Export all data** in your account settings emails you a link to a ZIP of every page as Markdown plus your uploaded images. The link lasts 24 hours. See [Exporting your data](/account/exporting-your-data). ### How long do deleted pages stay in the recycle bin? 30 days. After that they're deleted permanently. See [Recycle bin](/spaces/recycle-bin). ### Can I get an old version of a page back? Yes, if your plan includes version history. Folioso keeps daily snapshots of each page, and you can take your own, then preview and restore any of them. See [Version history](/editor/version-history). ### Something isn't working. Where do I start? See [Troubleshooting](/help/troubleshooting). --- # Troubleshooting Fixes for the most common problems, grouped by what you're seeing. Each one explains the likely cause and what to do about it. ## I can't invite anyone Inviting people is for organisation admins. If you're an admin and still can't send an invitation, check these in order. ### The invite button says Upgrade plan Your plan's seats are all in use. On a flat plan, **pending invitations count as well as members**, so open invitations may be what's filling the plan. The **Members** page shows "Every seat on your plan is taken." To fix it, do one of these: - **Revoke** a pending invitation you no longer need, under **Pending invitations**; - remove a member; - move to a per-seat plan, which adds a seat for each new member. See [Upgrading](/billing/upgrading). See [Seats](/billing/seats) for exactly who counts. ### You're asked to verify your email Sending invitations is locked until your email address is confirmed. You'll see "Verify your email" with "Sharing pages and sending invitations are locked until it's confirmed." Open the link in the verification email. If you can't find it, select **Resend verification email** for a fresh one. ### You're asked to set up two-factor authentication Admins of an organisation with other members must use two-factor authentication, so Folioso asks you to set it up before you invite your first member: "Admins of an organisation with other members must use two-factor authentication. Set it up before inviting your first member." Follow **Set it up**, then come back to the **Members** page. See [Two-factor authentication](/account/two-factor-authentication). ## An invitation email hasn't arrived 1. **Check the spam or junk folder** of the address the invitation was sent to. 2. **Check the address.** The **Members** page lists each pending invitation with its email address. If it's wrong, select **Revoke** and invite the right address. 3. **Check whether it has expired.** Invitations last 7 days. An expired one shows "expired *date*" on the **Members** page. 4. **Send it again.** An admin can select **Resend** next to the invitation. This sends a new link and restarts the 7 days. The link in any earlier email stops working, so use the newest one. If the invitee opens the link while signed in to a different account, they'll see "This invitation is for a different account". They should select **Sign out and switch account**, then open the link again with the account the invitation was sent to. If the invitation link says the organisation has no free seats, an admin needs to free a seat first. See [Seats](/billing/seats#when-there-are-no-free-seats). ## My account is suspended You'll see "Your account in *organisation* is suspended" when you open that organisation. Your account and pages are kept, and any other organisations you belong to work as normal; the page lists them under **Your other organisations**. ![The suspended page, with Your other organisations and Sign out](/images/docs/screens/suspended-light.webp) A member is suspended for one of three reasons: - an admin suspended you by hand; - the organisation suspends members automatically after a period without visiting; - the organisation moved to a plan with fewer seats than members. See [Members over the seat limit](/billing/returning-to-free#members-over-the-seat-limit). Only an admin of that organisation can unsuspend you, from the **Members** page, so contact one of them. On a full flat plan they'll need to free a seat or upgrade first. While you're suspended you don't receive email from that organisation. See [Suspending members](/organisation/suspending-members). ## An organisation address shows "page not found" Organisation addresses look like `https://your-organisation.folioso.dev`. If one shows "page not found": - **The address was changed.** When an admin changes the organisation's address, the old one redirects to the new one for 30 days, then stops working. Ask an admin for the new address and update your bookmarks. See [Organisation settings](/organisation/settings). - **You're not a member.** Folioso shows "page not found" rather than "access denied" for organisations you don't belong to, so it doesn't reveal which organisations exist. This includes an organisation you've left or been removed from. Ask an admin to invite you. - **The organisation was deleted.** A deleted organisation's address stops working straight away. See [Deleting an organisation](/organisation/deleting-an-organisation). If you belong to several organisations, check you're using the right one. See [Working in several organisations](/organisation/multiple-organisations). ## I've lost my two-factor device On the sign-in code screen, select **Lost your device? Use a recovery code** and enter one of the recovery codes you saved when you set up two-factor authentication. Each code works once. Once you're signed in: 1. Go to your account settings and find **Security**, which shows how many unused recovery codes you have left. 2. Select **Manage two-factor**. 3. Either select **Regenerate recovery codes** for a fresh set, or **Turn off** two-factor and set it up again on your new device. Save new recovery codes somewhere safe: they're shown only once. > [!WARNING] > Without your authenticator app or a recovery code, you can't complete the two-factor step at sign-in. Keep your recovery codes somewhere other than your phone. See [Two-factor authentication](/account/two-factor-authentication). ## Search doesn't find something I just wrote - **Page titles** are searchable straight away. - **Page content** is added to search in the background, about 30 to 40 minutes after the last edit. If you've just written something, try again a little later. Some searches don't match even once a page is indexed: - **Dotted names and paths**, such as `foo.bar` or `app/Models/Page.php`. Search for one part instead, such as `Page`. - **Mixing ordinary words with exact code names** in one search. Try fewer words, or search for each separately. Search only shows pages you're allowed to open. See [Search](/sharing/search). ## A public link asks for a password The person who shared the page has protected the link with a password. The page shows "Password required". Enter the password they gave you in **Link password**. - **The password doesn't work.** Check with the person who shared it. If they've changed the password, the old one no longer works, and anyone who unlocked the page with it has to enter the new one. - **You're told to wait.** After several wrong passwords, Folioso makes you wait before you can try again. - **You can view but it asks again.** The page is open to read, and the password unlocks edit access: "You can already view this page. The password you were given unlocks edit access." If you shared the page, you can **Change password** or **Remove password** in the page's share dialog. See [Public links](/sharing/public-links). ## My export email hasn't arrived or the link doesn't work **Data export.** When you select **Export all data** in your account settings, Folioso builds a ZIP of every page as Markdown plus your uploaded images and emails you a download link when it's ready. - It can take a while to build. Check your spam folder too. - The email goes to your account's email address. To check which one that is, see [Profile and email](/account/profile). - You need to be signed in to the same account to download it. - The link lasts 24 hours, after which the archive is deleted. Request a new export if it has expired. See [Exporting your data](/account/exporting-your-data). **PDF export.** A page's PDF is emailed as "Your PDF is ready". The link only works for your account, so forwarding it won't give anyone access, and the file is kept for 7 days. See [Exporting pages](/editor/exporting).