Uses atcute to show how you can do upserts with atproto records

upsert

+247 -2
+5
.env.template
··· 1 + # Please use your handle cause it uses it in the example 2 + HANDLE=baileytownsend.dev 3 + #I'm not adding oauth for an example 4 + APP_PASSWORD=555-555-5555 5 + PDS_HOST=https://yourpds.com
+1
.gitignore
··· 1 1 node_modules 2 2 .idea 3 + .env
+38
README.md
··· 1 + # ATProto Upserts 2 + 3 + Example repo showing you how to upsert data to the PDS. This will have an attached leaflet I'll add later. 4 + 5 + Setup 6 + Make a copy of [.env.template](.env.template) and rename it to .env. Fill it out. Make sure to use your handle. 7 + 8 + ```bash 9 + (p)npm install 10 + # Runs the tid example 11 + (p)npm run eample:tid 12 + # Runs the upsert example 13 + (p)npm run example:upsert 14 + ``` 15 + 16 + [./tid.js](./tid.js) 17 + Shows how you can create a TID from a datetime and convert it back 18 + ```shell 19 + Its 1/15/2026, 9:57:18 PM or 1768535838046 20 + TID: 3mcj7faghtk2r 21 + Converted back: 1/15/2026, 9:57:18 PM or 1768535838046 22 + ``` 23 + 24 + [./upsert.js](./upsert.js) 25 + Shows how you can upsert data to the PDS using the TID from a known date to upsert. 26 + 27 + ```shell 28 + You just finished a run. Uploading it to the PDS. 29 + Uploaded activity with rkey: 3mcj75qpg7c2r 30 + The PDS shows you went on a run at 2026-01-16T03:53:06.681Z. 31 + You just finished a walk. Uploading it to the PDS. 32 + Uploaded activity with rkey: 3mcj75qpg7c2r 33 + Uploaded activity with rkey: 3mcj75qspoc2r 34 + Since you did an upsert you should only have 2 records even tho you uploaded 3. 35 + You have 2 activities 36 + The PDS shows you went on a walk at 2026-01-16T03:53:06.789Z. 37 + The PDS shows you went on a run at 2026-01-16T03:53:06.681Z. 38 + ```
+6 -2
package.json
··· 5 5 "type": "module", 6 6 "main": "index.js", 7 7 "scripts": { 8 - "example:tid": "node ./tid.js" 8 + "example:tid": "node ./tid.js", 9 + "example:upsert": "node ./upsertFullExample.js" 9 10 }, 10 11 "keywords": [], 11 12 "author": "", 12 13 "license": "ISC", 13 14 "packageManager": "pnpm@10.16.1", 14 15 "dependencies": { 15 - "@atcute/tid": "^1.1.1" 16 + "@atcute/atproto": "^3.1.10", 17 + "@atcute/client": "^4.2.1", 18 + "@atcute/tid": "^1.1.1", 19 + "dotenv": "^17.2.3" 16 20 } 17 21 }
+81
pnpm-lock.yaml
··· 8 8 9 9 .: 10 10 dependencies: 11 + '@atcute/atproto': 12 + specifier: ^3.1.10 13 + version: 3.1.10 14 + '@atcute/client': 15 + specifier: ^4.2.1 16 + version: 4.2.1 11 17 '@atcute/tid': 12 18 specifier: ^1.1.1 13 19 version: 1.1.1 20 + dotenv: 21 + specifier: ^17.2.3 22 + version: 17.2.3 14 23 15 24 packages: 25 + 26 + '@atcute/atproto@3.1.10': 27 + resolution: {integrity: sha512-+GKZpOc0PJcdWMQEkTfg/rSNDAAHxmAUGBl60g2az15etqJn5WaUPNGFE2sB7hKpwi5Ue2h/L0OacINcE/JDDQ==} 28 + 29 + '@atcute/client@4.2.1': 30 + resolution: {integrity: sha512-ZBFM2pW075JtgGFu5g7HHZBecrClhlcNH8GVP9Zz1aViWR+cjjBsTpeE63rJs+FCOHFYlirUyo5L8SGZ4kMINw==} 31 + 32 + '@atcute/identity@1.1.3': 33 + resolution: {integrity: sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng==} 34 + 35 + '@atcute/lexicons@1.2.6': 36 + resolution: {integrity: sha512-s76UQd8D+XmHIzrjD9CJ9SOOeeLPHc+sMmcj7UFakAW/dDFXc579fcRdRfuUKvXBL5v1Gs2VgDdlh/IvvQZAwA==} 16 37 17 38 '@atcute/tid@1.1.1': 18 39 resolution: {integrity: sha512-djJ8UGhLkTU5V51yCnBEruMg35qETjWzWy5sJG/2gEOl2Gd7rQWHSaf+yrO6vMS5EFA38U2xOWE3EDUPzvc2ZQ==} ··· 20 41 '@atcute/time-ms@1.0.0': 21 42 resolution: {integrity: sha512-iWEOlMBcO3ktB+zQPC2kXka9H/798we+IWq2sjhb+hQJNNfcJrwejzvNi/68Q3jKo/hdfwZjRU9iF8U6D32/2Q==} 22 43 44 + '@atcute/uint8array@1.0.6': 45 + resolution: {integrity: sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A==} 46 + 47 + '@atcute/util-text@0.0.1': 48 + resolution: {integrity: sha512-t1KZqvn0AYy+h2KcJyHnKF9aEqfRfMUmyY8j1ELtAEIgqN9CxINAjxnoRCJIFUlvWzb+oY3uElQL/Vyk3yss0g==} 49 + 50 + '@badrap/valita@0.4.6': 51 + resolution: {integrity: sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==} 52 + engines: {node: '>= 18'} 53 + 54 + '@standard-schema/spec@1.1.0': 55 + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} 56 + 23 57 '@types/node@22.19.7': 24 58 resolution: {integrity: sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==} 59 + 60 + dotenv@17.2.3: 61 + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} 62 + engines: {node: '>=12'} 63 + 64 + esm-env@1.2.2: 65 + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} 25 66 26 67 node-gyp-build@4.8.4: 27 68 resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} ··· 30 71 undici-types@6.21.0: 31 72 resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} 32 73 74 + unicode-segmenter@0.14.5: 75 + resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==} 76 + 33 77 snapshots: 34 78 79 + '@atcute/atproto@3.1.10': 80 + dependencies: 81 + '@atcute/lexicons': 1.2.6 82 + 83 + '@atcute/client@4.2.1': 84 + dependencies: 85 + '@atcute/identity': 1.1.3 86 + '@atcute/lexicons': 1.2.6 87 + 88 + '@atcute/identity@1.1.3': 89 + dependencies: 90 + '@atcute/lexicons': 1.2.6 91 + '@badrap/valita': 0.4.6 92 + 93 + '@atcute/lexicons@1.2.6': 94 + dependencies: 95 + '@atcute/uint8array': 1.0.6 96 + '@atcute/util-text': 0.0.1 97 + '@standard-schema/spec': 1.1.0 98 + esm-env: 1.2.2 99 + 35 100 '@atcute/tid@1.1.1': 36 101 dependencies: 37 102 '@atcute/time-ms': 1.0.0 ··· 41 106 '@types/node': 22.19.7 42 107 node-gyp-build: 4.8.4 43 108 109 + '@atcute/uint8array@1.0.6': {} 110 + 111 + '@atcute/util-text@0.0.1': 112 + dependencies: 113 + unicode-segmenter: 0.14.5 114 + 115 + '@badrap/valita@0.4.6': {} 116 + 117 + '@standard-schema/spec@1.1.0': {} 118 + 44 119 '@types/node@22.19.7': 45 120 dependencies: 46 121 undici-types: 6.21.0 47 122 123 + dotenv@17.2.3: {} 124 + 125 + esm-env@1.2.2: {} 126 + 48 127 node-gyp-build@4.8.4: {} 49 128 50 129 undici-types@6.21.0: {} 130 + 131 + unicode-segmenter@0.14.5: {}
+116
upsertFullExample.js
··· 1 + import 'dotenv/config'; 2 + import { Client, CredentialManager, ok } from '@atcute/client'; 3 + import * as TID from '@atcute/tid'; 4 + const CLOCK_ID = 23; 5 + 6 + const logErrorAndThrow = (err) => { 7 + console.error(err); 8 + throw err; 9 + }; 10 + 11 + 12 + //Loading in env variables can ignore 13 + const handle = process.env.HANDLE; 14 + if (!handle) logErrorAndThrow('HANDLE is not set'); 15 + const appPassword = process.env.APP_PASSWORD; 16 + if (!appPassword) logErrorAndThrow('APP_PASSWORD is not set'); 17 + const pdsHost = process.env.PDS_HOST; 18 + if (!pdsHost) logErrorAndThrow('PDS_HOST is not set'); 19 + 20 + 21 + //Sets up the atcute client 22 + const manager = new CredentialManager({ service: pdsHost }); 23 + const rpc = new Client({ handler: manager }); 24 + 25 + // sign in with handle/email and password (or app password (please be an app password)) 26 + await manager.login({ identifier: handle, password: appPassword}); 27 + 28 + 29 + 30 + // The actual example starts here, everything else before is boilerplate 31 + 32 + 33 + //reversing your handle to make a fun example lexicon 34 + const reversedHandle = handle.split('.').reverse().map(word => word).join('.'); 35 + //An activity here is like a workout. running, walking, lifting weights, etc. 36 + let collection = `${reversedHandle}.feed.activity` 37 + 38 + //A list of activities that may be gotten from your phone or wherever, but you get the whole list everytime 39 + let activities = [] 40 + 41 + // Helper function to upload activities 42 + const uploadActivities = async (activities, handle, collection, rpc) => { 43 + 44 + for (const activity of activities) { 45 + 46 + //Creates that unique key from the startTime of the activity so we don't have duplicates 47 + let rKey = TID.create(activity.startTime.getTime() * 1000, CLOCK_ID); 48 + 49 + await ok(rpc.post('com.atproto.repo.putRecord', { 50 + input: { 51 + repo: handle, 52 + collection, 53 + rkey: rKey, 54 + record: activity, 55 + 56 + } 57 + })); 58 + console.log(`Uploaded activity with rkey: ${rKey}`); 59 + } 60 + 61 + } 62 + 63 + 64 + //I just finished a run, it's saved to my phone, now uploading it to the PDS 65 + activities.push({ 66 + $type: collection, 67 + type: 'run', 68 + startTime: new Date() 69 + }) 70 + 71 + console.log('You just finished a run. Uploading it to the PDS.'); 72 + 73 + //Upload my activities 74 + await uploadActivities(activities, handle, collection, rpc); 75 + 76 + //Going to generate the key here to show you can find the record from it easily if you have a date 77 + const rkey = TID.create(activities[0].startTime.getTime() * 1000, CLOCK_ID); 78 + 79 + const activityFromPDS = await ok(rpc.get('com.atproto.repo.getRecord', { 80 + params: { 81 + repo: handle, 82 + collection, 83 + rkey, 84 + } 85 + })); 86 + 87 + console.log(`The PDS shows you went on a ${activityFromPDS.value.type} at ${activityFromPDS.value.startTime.toLocaleString()}.`); 88 + 89 + 90 + //I decide I want to go on a walk later, so I add it to the list 91 + activities.push({ 92 + $type: collection, 93 + type: 'walk', 94 + startTime: new Date() 95 + }) 96 + 97 + console.log('You just finished a walk. Uploading it to the PDS.'); 98 + 99 + //upload the new activities so the newest one can sync 100 + await uploadActivities(activities, handle, collection, rpc); 101 + 102 + const listResult = await ok(rpc.get('com.atproto.repo.listRecords', { 103 + params: { 104 + repo: handle, 105 + collection, 106 + limit: 10, 107 + } 108 + })); 109 + 110 + 111 + console.log("Since you did an upsert you should only have 2 records even tho you uploaded 3.") 112 + console.log(`You have ${listResult.records.length} activities saved in the PDS.`); 113 + for (const recordIndex in listResult.records){ 114 + const record = listResult.records[recordIndex] 115 + console.log(`The PDS shows you went on a ${record.value.type} at ${record.value.startTime.toLocaleString()}.`); 116 + }