{"id":22480238,"url":"https://github.com/pinatacloud/pinapple","last_synced_at":"2026-02-19T14:31:50.151Z","repository":{"id":259290894,"uuid":"874990417","full_name":"PinataCloud/pinapple","owner":"PinataCloud","description":null,"archived":false,"fork":false,"pushed_at":"2024-10-23T19:08:21.000Z","size":466,"stargazers_count":2,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-10-21T00:06:26.419Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://pinapple.cloud","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/PinataCloud.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-10-18T21:08:19.000Z","updated_at":"2025-08-26T12:18:46.000Z","dependencies_parsed_at":"2024-10-24T05:30:37.943Z","dependency_job_id":"ccefd161-c6af-49b3-b3fa-23928a0021a0","html_url":"https://github.com/PinataCloud/pinapple","commit_stats":null,"previous_names":["pinatacloud/pinapple"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/PinataCloud/pinapple","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PinataCloud%2Fpinapple","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PinataCloud%2Fpinapple/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PinataCloud%2Fpinapple/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PinataCloud%2Fpinapple/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/PinataCloud","download_url":"https://codeload.github.com/PinataCloud/pinapple/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/PinataCloud%2Fpinapple/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29618271,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-19T13:04:20.082Z","status":"ssl_error","status_checked_at":"2026-02-19T13:03:33.775Z","response_time":117,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-12-06T15:20:14.123Z","updated_at":"2026-02-19T14:31:50.134Z","avatar_url":"https://github.com/PinataCloud.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🍍 Pinapple - An old school Macintosh-inspired file system on the web\n\nPinapple is a browser-based file system that supports all file types. You can create folders, drag items, add, and delete. Just like you would on an old Macintosh. \n\n## Why does this exist?\n\nThis is a demonstration of what you can do with the [Pinata Files API](https://pinata.cloud). The entire app is powered by this API. There is no database, and the only other dependency is Clerk for authentication. \n\nAlso, it's fun. \n\n## Building Pinapple\n\nHello, [Pinapple](https://pinapple.cloud).\n\nRetro is always in. This is what I thought when I decided to show off [the Pinata Files API](https://pinata.cloud/blog/introducing-the-internets-files-api/) by building an in-browser replica of the classic Macintosh desktop. The goal was to have simple file management functionality. You could drag new files in, create folders, and move files and folders all around the desktop. And it would look like you were simply using a classic Macintosh, but in the browser. \n\nThis isn’t going to be a full tutorial, but the code is available and it’s open source. Instead, this post will explore how I used the Pinata Files API to make this work. The entire app is built with just two dependencies—Clerk for authentication and Pinata for everything else. \n\nBefore diving into how I built it, let’s talk about the name.\n\nPinapple is a play on Pinata (which powers the file management) and Apple (which is apple). I pronounce it pin-apple, but you can call it pineapple, if you’d prefer. I won’t judge you. \n\nAnyway, I want to talk a bit about the UI and then I’ll show off some of the nitty gritty code.  \n\n### System.css\n\nWhen I first had the idea to build a Macintosh-styled interface, I was desperately hoping there was a CSS library that made it simple. I did not like the idea of having to create such an interface while writing raw CSS myself. \n\nFortunately, I came across [system.css](https://github.com/sakofchit/system.css/tree/main). The library is perfect. It doesn’t do a lot, but it does enough. And the look and feel is perfect for what I wanted to build. Rather than use a CDN or install the library from NPM, I just copied the CSS into the project’s main CSS file. This gave me flexibility. At least, that’s what I told myself, because I knew I wanted to combine system.css with Tailwind. \n\n### Tailwind CSS\n\nFor me, choosing Tailwind was simply an acceleration decision. I wanted to move fast with this project, and I could use Tailwind to handle a lot of what I consider boilerplate CSS, like font sizes and flex box positioning. \n\nBecause Tailwind is configurable and compatible with any other CSS you write, I was able to write my own CSS for things a little more complex than I like to use Tailwind for, such as animations. I was also able to easily combine Tailwind with system.css. \n\n### Architecture\n\nThe architectural design of the app is pretty simple. Users would access their user profile (i.e. log in) and those user profiles would be associated with their specific files. It was important that users would not see each other’s files. So, when Alice logs in, she should not see Bob’s files. \n\nFile uploads are handled by Pinata. Every file is stored using Pinata’s Private Files API. Users needed to be able to drag files from their actual desktop to this virtual in-browser desktop. I also wanted them to be able to click a button to upload, so I added a link to the main file menu in the interface’s menu bar. \n\nFolders are a critical part of a desktop, so users had to be able to create folders. To keep things simple, I limited folder creation to one level deep, so nested folders would not be permitted. When a folder is opened, users should be able to drag new files into the folder. They should also be able to re-arrange files in the folder. \n\nWhen users re-arranged files on the desktop or in a folder, the positions should be remembered, even if the user refreshes the browser. \n\nFinally, when a user double clicks (or right-clicks and selects open) a file, it should open in a window and be viewable if it’s an image or a video. Otherwise, it should download to the user’s computer. \n\nTo manage all of this and ensure each person’s files and folders were specific to them, we used Clerk’s authentication middleware, and we scoped file uploads and folder creation to specific users. \n\nLet’s see some examples in code. \n\n### The Code\n\nAgain, this is not a tutorial, but we’ll look at some of the code for the project to get a sense for how the Pinata Files API enables everything I did in the app. \n\n#### One-time  use keys\n\nIn order to upload from the client side, I created an API endpoint dedicated to generating one-time use keys that the client could then use. \n\nBackend code: \n\n```jsx\nlet { userId } = getAuth(req)\n\nif (!userId) {\n  //  check if local test user\n  const testUser = await getTestUser(req.headers.authorization?.split(\"Bearer \")[1] || \"\")\n  if(!testUser) {\n    return res.status(401).json({ error: 'Not authenticated' })\n  } else {\n    userId = req.headers.authorization?.split(\"Bearer \")[1] as string\n  }     \n}\n\nconst keyData = await pinata.keys.create({ keyName: `${userId}+${Date.now()}`, permissions: { admin: true }, maxUses: 1 })\n\nres.json({ data: keyData.JWT });\n```\n\nThere’s some middleware in there to check if the user is authenticated via our Clerk auth software or if they are signed in as a guest user. Otherwise, it’s a one-liner to generate a one-time use key. \n\nFrontend code: \n\n```jsx\nconst handleUpload = async (fileData: any, groupId?: string) =\u003e {\n  try {\n    let headers: any = {\n      'Content-Type': 'application/json'\n    }\n    if(!user?.id) {\n      headers.authorization = `Bearer ${getLocalUserId()}`\n    }\n    const keyRes = await fetch(\"/api/files\", {\n      method: \"POST\", \n      headers \n    })\n\n    const keyData = await keyRes.json()\n    const key = keyData.data;\n    // Upload from the client\n    if(groupId) {\n      await pinata.upload.file(fileData).addMetadata({ name: `${user?.id}+${fileData.name}`, keyvalues: {\n        userId: user?.id || getLocalUserId() || \"\", \n        testUser: user?.id ? \"false\" : \"true\"\n      } }).group(groupId).key(key)\n    } else {\n      await pinata.upload.file(fileData).addMetadata({ name: `${user?.id}+${fileData.name}`, keyvalues: {\n        userId: user?.id || getLocalUserId() || \"\", \n        testUser: user?.id ? \"false\" : \"true\"\n      } }).key(key)\n    }\n  } catch (error) {\n    throw error;\n  }\n}\n```\n\nThe frontend does the heavy lifting. We are either uploading to a folder or simply adding a file to the “desktop.” This code has logic to support both. We use [Pinata’s Groups](https://pinata.cloud/blog/organize-your-ipfs-files-with-groups/) feature to create the illusion of folders. You can see how we attach a `groupId` if a file belongs in a folder. We also attach the one-time use key. \n\nSpeaking of folders, let’s see how that works. \n\n#### Folders\n\nFolders are simply Pinata Groups. But to make them unique across all users, I needed to do a little trickery. \n\n```jsx\nconst { groupName } = req.body\nconst { userId } = getAuth(req)\n\nif (!userId) {\n  //  check if local test user\n  const testUser = await getTestUser(req.headers.authorization?.split(\"Bearer \")[1] || \"\")\n  if(!testUser) {\n    return res.status(401).json({ error: 'Not authenticated' })\n  }        \n}\n\nawait pinata.groups.create({ name: `${userId}+${groupName}` });\nres.send(\"Success\");\n```\n\nBecause this app is built on top of my own Pinata developer account, I needed a way to distinguish groups for each user. If Alice creates a “Cats” group, Bob should also be able to create a group named “Cats.” This is where the concatenation of the userId allows for uniqueness. \n\n#### Dragging files into folders\n\nI used the same drag and drop logic that allows the user to move their files and folders all over the desktop and extended it to detect if a file is dragged over a folder. If it is, then when it’s dropped while over that folder, this function is called: \n\n```jsx\nconst handleAddToFolder = async (fileId: string, folderId: string) =\u003e {\n  let headers: any = {\n    'Content-Type': 'application/json'\n  }\n  if(!user?.id) {\n    headers.authorization = `Bearer ${getLocalUserId()}`\n  }\n  await fetch(`/api/groups`, {\n    method: \"PUT\", \n    headers: headers, \n    body: JSON.stringify({\n      fileId, \n      folderId\n    })\n  })\n\n  loadFiles();\n}\n```\n\nOn the backend, I handle it like this: \n\n```jsx\nconst { userId } = getAuth(req)\n\nif (!userId) {\n  //  check if local test user\n  const testUser = await getTestUser(req.headers.authorization?.split(\"Bearer \")[1] || \"\")\n  if(!testUser) {\n    return res.status(401).json({ error: 'Not authenticated' })\n  }        \n}\n\nconst { fileId, folderId } = req.body;\nawait pinata.groups.addFiles({groupId: folderId, files: [fileId] });\nres.send(\"Success\")\n```\n\nPretty simple! One final trick I’ll show is how I make sure the files in groups do not show up on the desktop. This one is dead-simple. \n\n```jsx\nawait pinata.files.list().metadata({ userId: userId }).noGroup(true)\n```\n\nWith this one line, I can list all the files for a user that are not in a folder (i.e. a group). \n\n### What’s next?\n\nI don’t know, have fun with it. This was just a cool idea I had, and I was like “I bet I can build this in like a day.”\n\nIt took me two days, but you know how estimates go. \n\nThe code is MIT-licensed, except for system.css which is Apache-licensed. So, go to town. Build your own Macintosh desktop, or do something completely different.\n\n## Credits\n\nThe app is built using the incredible [system.css stylesheet](https://github.com/sakofchit/system.css/tree/main) and Tailwind CSS. Icons were created by scratch by Pinata's amazing designer, Marjorie Doucet, and made to look as close to original Macintosh icons as possible. \n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpinatacloud%2Fpinapple","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpinatacloud%2Fpinapple","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpinatacloud%2Fpinapple/lists"}