Tutorial: Accessing CloudKit from React / Gatsby.js

Andrew Ho
4 min readApr 23, 2022


1. Add Cloudkit as plug-in to the gatsby-config.js:

module.exports = {

siteMetadata: {

title: `your domain`,siteUrl: `https://www.yourdomain.tld`,},

plugins: [

{resolve: ‘gatsby-plugin-load-script’,

options: {src: ‘https://cdn.apple-cloudkit.com/ck/2/cloudkit.js'


2. Configure the Cloudkit connector:


containers: [{

containerIdentifier: ‘your cloudkit container path’,

apiTokenAuth: {

apiToken: ‘your token from cloudkit dashboard’,

persist: true


environment: ‘development or production version of the database’


const container = window.CloudKit.getDefaultContainer()

const publicDB = container.publicCloudDatabase;

3. Log-in / Log-out, saving of authentication state in useContext

onAuthenticated() {



componentDidMount() {



.then(userInfo => {

if(userInfo) {

this.setState({userInfo: userInfo});






.then(userInfo => {

this.setState({userInfo: userInfo})






.then( x => {





4. Show the Apple Sign-In / -Out Buttons

return <div>

<div id=”apple-sign-in-button”></div>

<div id=”apple-sign-out-button”></div>


5. Query Cloudkit

getPersons() {

return publicDB.performQuery({recordType: ‘CD_Person’}).then((response) => {

var records = response.records;

var numberOfRecords = records.length;

if (numberOfRecords === 0) {

} else {

var myList = []

records.forEach(function (record) {

const fields = record.fields;

const fpID = fields[‘CD_personID’].value;


{“personID”: fpID,

“fullName”: fields[‘CD_fullName’].value




  • the mandatory parameter of the query is the recordType (given above)
  • the optional parameters of the query are: filterBy, sortBy

6. Transferring the query results to a React state variable

constructor() {


this.state = {

userInfo: null,

records: null,


componentDidMount() {

this.getPersons().then(res => {this.setState({records: res});})


7. Rendering the records


{this.state.records.map((item) => {

return (


<h3> { `Name: ${item.fullName} ///// fpID: ${item.personID}`}</h3>




8. filterBy: special case of systemFieldName / “createdUserRecordName”

  • This is needed to connect the logged in Apple user to entries in the Cloudkit database
  • The syntax happens to be different from other queries. It is unclear whether this is a bug or feature, which appears to have been known since 2016:
  • Note: make sure the “createdUserRecordName” of relevant record type has a queryable index added via the Cloudkit dashboard, otherwise the query will generate a 400 bad request error.

filterBy: [{

comparator: ‘EQUALS’,

systemFieldName: ‘createdUserRecordName’,

fieldValue: {

value: {recordName : appleUser,},

type: “REFERENCE”,


  • type appears to be optional. Type is included here because examples given by Apple included it.

9. filterBy for non-system fields

filterBy: [{

comparator: ‘EQUALS’,

fieldName: ‘CD_myfield’,

fieldValue: {

value: myfield_value,

type: “STRING”,


  • The nesting of value.recordName is not allowed. Using the nesting generates 400 bad request error.
  • type appears to be optional

10. Retrieving images

From a record with CD_photo as the name of the field:

thisURL = fields[‘CD_photo_ckAsset’].value.downloadURL,

After that the url can be used in any <img src=”thisURL”> tag.

11. Structure of the Response from Cloudkit

— list of [

  • recordName: aka the recordID, not sure why Apple chooses to use a different name
  • recordType: this is the name of the table
  • created: dictionary that documents who created this record and when
  • modified
  • recordChangeTag: this is used to version the particular record, for purpose of updating
  • fields — this contains a dictionary of fieldNames and associated values
  • deleted
  • zoneID
  • pluginFields

12. Update CloudKit record

const newRecord = {

recordType: ‘CD_Person’,

recordName: recordName, // this is the unique record ID that was previously assigned by CloudKit

fields: {

CD_city: {value: record.city}, // record.city is the new value

CD_country: {value: record.country},


Batch builder is the preferred way, and can support multiple sequential operations:

Use forceUpdate when you don’t care if there was an update that may have taken place between the last time you download this record and your currently intended update. Otherwise use “update”.

let success = publicDB.newRecordsBatch().forceUpdate(newRecord).commit()

.then(function(response) {

if (response.hasErrors) {

throw response.errors[0];

} else { console.log(response)}


return success

13. Updating image

ckAsset updating require an extra step to convert image file to blob because CloudKit requires uploading of blob format. Also, note that the field to be updated is the ckAsset field.

if (record.photo.length > 0) {

const blob = new Blob([record.photo[0]])

const photoRecord = {

recordType: ‘CD_Person’,

recordName: oldRecord.recordName,

fields: {

CD_photo_ckAsset: assetField,

}}; }

14. Delete record

const thisRecord = {

recordType: ‘CD_Person’,

recordName: recordName,


let success = publicDB.newRecordsBatch().forceDelete(thisRecord).commit()

15. Deploy to Web Server

Because Cloudkit is an object of web browser “window”, running “gatsby build” causes an “WebpackError: ReferenceError: window is not defined”.

The solution is to isolate the CloudKit code with conditional code:

const isBrowser = () => typeof window !== ‘undefined’

if ( isBrowser) {

const container = window.CloudKit.getDefaultContainer()

const publicDB = container.publicCloudDatabase;

return publicDB.performQuery(query, options).then((response) => {


  1. Apple Docs

2. writing query:

3. example code from Apple:

4. another version of example code:

5. Gatsby CloudKit example:

6. Error example

7. React example code

8. Apple Cloudkit JS Reference